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_t *,const std::nullptr_t *等。
那么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
 |