更多 C++ 习语/nullptr
区分整数 0 和空指针。
多年来,C++ 缺乏一个关键字来表示空指针,这令人尴尬。C++11 已经消除了这种尴尬。C++ 的强类型检查使得 C 中的 NULL 宏在表达式中几乎无用,例如:
// if the following were a valid definition of NULL in C++
#define NULL ((void *)0)
// then...
char * str = NULL; // Can't automatically convert void * to char *
void (C::*pmf) () = &C::func;
if (pmf == NULL) {} // Can't automatically convert from void * to pointer to member function.
相反,
#define NULL 0
或者
#define NULL 0L
是 C++ 中 NULL 的有效定义。见下文。
事实上,问题的关键在于 C++ 禁止从 void * 转换,即使该值是常量零,但,对于常量零,它仍然引入了一个特殊情况:int 到指针(实际上有几个:short 到指针,long 到指针等)。事实上,这甚至比允许(对于常量情况)之前的异常更糟糕,尤其是考虑到语言对函数重载的支持。
void func(int);
void func(double *);
int main()
{
func (static_cast <double *>(0)); // calls func(double *) as expected
func (0); // calls func(int) but the programmer might have intended double *, because 0 IS also a null pointer constant (or the reader might be misled).
}
使用宏 NULL 也存在它自己的问题。C++ 要求宏 NULL 被定义为一个值为 0 的整数常量表达式。因此,与 C 不同,NULL 不能在 C++ 标准库中定义为 (void *)0。此外,定义的具体形式留给具体的实现,这意味着例如 0 和 0L 都是可行的选项,还有其他一些选项。这会造成问题,因为它会导致重载解析的混乱。更糟糕的是,令人困惑的重载解析表现出来的方式会因编译器及其使用的设置而异。下面是上面的例子略微修改后的一个说明性例子
#include <cstddef>
void func(int);
void func(double *);
int main()
{
func (static_cast <double *>(0)); // calls func(double *) as expected
func (0); // calls func(int) but double * may be desired because 0 IS also a null pointer
func (NULL) // calls func(int) if NULL is defined as 0 (confusion, func(double *) was intended!) - logic error at runtime,
// but the call is ambiguous if NULL is defined as 0L (yet more confusion to the unwary!) - compilation error
}
nullptr 习语解决了一些上述问题,可以以可重用形式实现,作为库解决方案。它通过仅使用 C++11 标准之前的标准特性,非常接近“空关键字”。
以下是一个库解决方案,它主要来自 Scott Meyers 的《Effective C++》,第二版,第 25 条(这本书的第三版中没有)。
const // It is a const object...
class nullptr_t
{
public:
template<class T>
inline operator T*() const // convertible to any type of null non-member pointer...
{ return 0; }
template<class C, class T>
inline operator T C::*() const // or any type of null member pointer...
{ return 0; }
private:
void operator&() const; // Can't take address of nullptr
} nullptr = {};
以下代码展示了一些使用案例(并假设上面的类模板已经 #include 了)。
#include <typeinfo>
struct C
{
void func();
};
template<typename T>
void g( T* t ) {}
template<typename T>
void h( T t ) {}
void func (double *) {}
void func (int) {}
int main(void)
{
char * ch = nullptr; // ok
func (nullptr); // Calls func(double *)
func (0); // Calls func(int)
void (C::*pmf2)() = 0; // ok
void (C::*pmf)() = nullptr; // ok
nullptr_t n1, n2;
n1 = n2;
//nullptr_t *null = &n1; // Address can't be taken.
if (nullptr == ch) {} // ok
if (nullptr == pmf) {} // Valid statement; but fails on g++ 4.1.1-4.5 due to bug #33990
// for GCC 4: if ((typeof(pmf))nullptr == pmf) {}
const int n = 0;
if (nullptr == n) {} // Should not compile; but only Comeau shows an error.
//int p = 0;
//if (nullptr == p) {} // not ok
//g (nullptr); // Can't deduce T
int expr = 0;
char* ch3 = expr ? nullptr : nullptr; // ch3 is the null pointer value
//char* ch4 = expr ? 0 : nullptr; // error, types are not compatible
//int n3 = expr ? nullptr : nullptr; // error, nullptr can’t be converted to int
//int n4 = expr ? 0 : nullptr; // error, types are not compatible
h( 0 ); // deduces T = int
h( nullptr ); // deduces T = nullptr_t
h( (float*) nullptr ); // deduces T = float*
sizeof( nullptr ); // ok
typeid( nullptr ); // ok
throw nullptr; // ok
}
不幸的是,gcc 4.1.1 编译器似乎存在一个bug,它无法识别 nullptr 与指向成员函数(pmf)的比较。上面的代码可以在 VC++ 8.0 和 Comeau 4.3.10.1 beta 上编译。
请注意,nullptr 习语利用了返回值类型解析器 习语来自动推断正确类型的空指针,具体取决于它要分配到的实例的类型。例如,如果 nullptr 要分配给字符指针,则会创建一个字符类型的模板化转换函数的实例。
该技术有一些缺点,并在N2431 提案草案中进行了讨论。总而言之,缺点包括
- 必须包含头文件才能使用 nullptr。在 C++11 中,nullptr 本身是一个关键字,不需要头文件(尽管 std::nullptr_t 需要)。
- 历史上,编译器在使用提议的代码时会生成(可以说)令人不满意的诊断信息。
- ISO C++11
- Effective C++,第二版
- N2431:nullptr 提案