更多 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 提案