nullptr是指针类型吗?

nullptr是C++里预定义的一个变量,它的类型是std::nullptr_t。 判断一个类型是否是指针类型,可以用std::is_pointer来判断。 测试std::nullptr_t是否是指针类型的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <myostream.h>
myostream::ostream mycout(std::cout.rdbuf());
#define watch(...) MYOSTREAM_WATCH(mycout, " = ", "\n", "\n\n", __VA_ARGS__)

int main() {
    // 确认nullptr就是std::nullptr_t类型
    static_assert(std::is_same_v<decltype(nullptr), std::nullptr_t>, "Never happen");

    watch(std::is_pointer_v<std::nullptr_t>,
          std::is_member_pointer_v<std::nullptr_t>,
          std::is_pointer_v<std::nullptr_t *>
    );
    return 0;
}

输出:

1
2
3
std::is_pointer_v<std::nullptr_t> = 0
std::is_member_pointer_v<std::nullptr_t> = 0
std::is_pointer_v<std::nullptr_t *> = 1

可见std::nullptr_t并不是一个指针类型。 只不过我们平时常用nullptr来赋值给任意指针类型,会给人一种std::nullptr_t是指针类型的错觉。

从上例中还可以看出std::nullptr_t *等类型是指针类型,它们是指向std::nullptr_t的指针类型。

原理

std::nullptr_t不是指针类型,但是却可以转换成任意指针类型,并且以0为值。 它的一种实现方式如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#ifdef _LIBCPP_HAS_NO_NULLPTR

_LIBCPP_BEGIN_NAMESPACE_STD

struct _LIBCPP_TEMPLATE_VIS nullptr_t
{
    void* __lx;

    struct __nat {int __for_bool_;};

    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t() : __lx(0) {}
    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t(int __nat::*) : __lx(0) {}

    _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR operator int __nat::*() const {return 0;}

    template <class _Tp>
        _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR
        operator _Tp* () const {return 0;}

    template <class _Tp, class _Up>
        _LIBCPP_INLINE_VISIBILITY
        operator _Tp _Up::* () const {return 0;}

    friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator==(nullptr_t, nullptr_t) {return true;}
    friend _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR bool operator!=(nullptr_t, nullptr_t) {return false;}
};

inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR nullptr_t __get_nullptr_t() {return nullptr_t(0);}

#define nullptr _VSTD::__get_nullptr_t()

_LIBCPP_END_NAMESPACE_STD

#else  // _LIBCPP_HAS_NO_NULLPTR

namespace std
{
    typedef decltype(nullptr) nullptr_t;
}

#endif // _LIBCPP_HAS_NO_NULLPTR

可见std::nullptr_t不是指针类型,可能是类似类的类型。 所以当然可以构造出指向std::nullptr_t的指针类型了,例如std::nullptr_ *const std::nullptr_ *等。 那么std::nullptr_t是类类型吗?

nullptr是类类型吗?

通过以上源码可以看出std::nullptr_t通过宏开关设置了两种实现方式, 第一种就是一个普通的struct,是类类型,而第二种似乎是编译器内置的,不是类类型。 所以std::nullptr_t的类型是依赖实现的,可能是类类型,也可能不是。

判断一个类型是否是类类型,可用std::is_class。测试代码如下:

1
2
3
4
5
6
7
8
watch(std::is_class_v<std::nullptr_t>,
    std::is_union_v<std::nullptr_t>,
    std::is_null_pointer_v<std::nullptr_t>,
    std::is_scalar_v<std::nullptr_t>,
    std::is_object_v<std::nullptr_t>);

watch(std::is_null_pointer_v<std::nullptr_t>,
    std::is_null_pointer_v<void*>);

Clang和GCC在 -std=c++17 下默认输出是一样的:

1
2
3
4
5
6
7
8
std::is_class_v<std::nullptr_t> = 0
std::is_union_v<std::nullptr_t> = 0
std::is_null_pointer_v<std::nullptr_t> = 1
std::is_scalar_v<std::nullptr_t> = 1
std::is_object_v<std::nullptr_t> = 1

std::is_null_pointer_v<std::nullptr_t> = 1
std::is_null_pointer_v<void*> = 0

可见默认情况下std::nullptr_t也不是类类型,它就是独特的一个类型,可用std::is_null_pointer来判断。

(当然,如果在Clang下编译时手动添加宏定义_LIBCPP_HAS_NO_NULLPTR,那么std::is_class_v<std::nullptr_t>就等于true了,它就是普通的类类型了。)

nullptr能被取地址吗?

nullptr是右值临时变量,无法取地址,但是我们可以使用std::nullptr_t重新定义一个新的"nullptr"左值,然后就可以对其取地址了。代码如下:

1
2
3
4
5
// watch(nullptr, &nullptr); // error: cannot take the address of an rvalue of type 'nullptr_t'

std::nullptr_t null1, null2;
watch(null1, &null1);
watch(null2, &null2);

输出:

1
2
3
4
5
null1 = nullptr
&null1 = 0x16b1d3578

null2 = nullptr
&null2 = 0x16b1d3570

如何用C++的方式把"T*"转换成"void*"

假如T是某种未知的模版类型,如何把T*的变量转换成void*类型呢? 用C语言的强制类型转换很容易实现:

1
2
3
4
template <typename T>
void* c_cast(T* p) { // 使用C风格的强制类型转换
    return (void*)(p);
}

那么如何用C++的类型转换方式实现呢?

事实上只用reinterpret_cast或static_cast是不行的,因为它们不能将 cv-qualified pointer 转换成 cv-unqualified pointer。 所以必须首先使用const_cast将cv属性去掉,然后再使用reinterpret_cast或static_cast。如下:

1
2
3
4
5
6
7
template <typename T>
void* cpp_cast(T* p) { // 使用C++风格的强制类型转换
    // return reinterpret_cast<void*>(p); // error
    return reinterpret_cast<void*>(const_cast<std::remove_cv_t<T>*>(p)); // OK
    // 或者用static_cast亦可
    // return static_cast<void*>(const_cast<std::remove_cv_t<T>*>(p)); // OK
}

cv-qualified std::nullptr_t及其指针

经过以上讨论,知道std::nullptr_t不是指针类型,那么如下两个问题就可迎刃而解。

cv-qualified std::nullptr_t 类型的变量能直接赋值给 std::nullptr_t 类型的变量吗?

可以。

1
2
const volatile std::nullptr_t p{0};
std::nullptr_t np = p; // OK

cv-qualified std::nullptr_t * 类型的变量能直接赋值给 std::nullptr_t * 类型的变量吗?

不可以。需要用const_cast转换后方可赋值。

1
2
3
const volatile std::nullptr_t * p{0};
// std::nullptr_t* np = p; // Error
std::nullptr_t* np = const_cast<std::nullptr_t*>(p); // OK