Item 8:優先使用 nullptr 而非 0 與 NULL (Prefer nullptr to 0 and NULL)

Overview Table

主題 重點
核心建議 想表達 null pointer 時一律用 nullptr,不要用 0NULL
0 的本質 0int,不是指標;只有在「非指標不可」的語境才勉強被解讀為 null pointer
NULL 的本質 實作可定義為任意整數型別(00L…),同樣不是指標型別
nullptr 型別 std::nullptr_t(循環定義:nullptr 的型別),可隱式轉換為所有 raw pointer 型別
優點 1 避免整數/指標 overload resolution 意外
優點 2 提升程式碼清晰度(尤其搭配 auto 時,== nullptr 明示為指標)
優點 3 template type deduction 對 0/NULL 推導出整數型別而非指標,nullptr 則沒有這個問題
配套守則 避免同時 overload 整數型別與指標型別(C++98 的舊守則在 C++11 仍然有效)

為什麼 0 和 NULL 不是指標

C++ 的基本原則0int。只有當語境要求指標時,編譯器才退而求其次把 0 當作 null pointer——這是 fallback,不是本意。NULL 也一樣:標準允許實作給它 int 以外的整數型別(如 0L),但重點是兩者都沒有指標型別

由此導致 overload resolution 的意外:

void f(int);      // f 的三個 overload
void f(bool);
void f(void*);

f(0);             // 呼叫 f(int),絕不會呼叫 f(void*)!

f(NULL);          // 可能編譯失敗;若 NULL 是 0L,
                  // long→int、long→bool、0L→void* 三種轉換同樣好 → 歧義
                  // 通常呼叫 f(int),絕不會呼叫 f(void*)

f(nullptr);       // 呼叫 f(void*) —— nullptr 無法被視為任何整數型別

原始碼「看起來的語意」(我在傳 null pointer)與「實際語意」(我在傳某種整數)互相矛盾——這正是 C++98 時代「不要同時 overload 整數與指標型別」守則的由來,且該守則在 C++11 依然成立,因為總有人繼續寫 0NULL

0 / NULL ──(本質是整數)──▶ 優先匹配 f(int)/f(bool)   ✘ 意外
             │
             └─(僅 fallback)─▶ null pointer

nullptr ──nullptr_t)──▶ 隱式轉換到所有指標型別 ──▶ f(void*  ✔
             │
             └─✘ 無法轉換為整數型別
例外與細節

  • nullptr 嚴格來說也不是指標型別,其真正型別是 std::nullptr_t;因為它能隱式轉換為所有 raw pointer 型別,才「表現得像所有型別的指標」。
  • NULL 的確切型別依實作而異(00L 等),因此 f(NULL) 的行為(歧義或呼叫整數版)在不同平台可能不同。

nullptr 提升可讀性(搭配 auto)

當回傳型別不明顯時,與 0 比較無法看出變數是指標還是整數;與 nullptr 比較則毫無歧義:

auto result = findRecord(/* arguments */);

if (result == 0) {        // result 是指標?還是整數?看不出來
  …
}

if (result == nullptr) {  // 一目了然:result 必定是指標型別
  …
}

template 中的 0/NULL:最致命的場景

假設有三個必須在鎖住 mutex 後才能呼叫的函式,各自接受不同的指標:

int    f1shared_ptr<Widget> spw;  // 需先鎖對應 mutex
double f2unique_ptr<Widget> upw;
bool   f3(Widget* pw);

template<typename FuncType, typename MuxType, typename PtrType>
decltype(auto) lockAndCall(FuncType func,   // C++14 版本
                           MuxType& mutex,  // (C++11 用尾置回傳型別
                           PtrType ptr)     //   -> decltype(func(ptr)))
{
  MuxGuard g(mutex);   // MuxGuard = std::lock_guard<std::mutex>
  return func(ptr);    // 鎖住 → 呼叫 → 解鎖
}

auto result1 = lockAndCall(f1, f1m, 0);        // 錯誤!ptr 被推導為 int
auto result2 = lockAndCall(f2, f2m, NULL);     // 錯誤!ptr 被推導為整數型別
auto result3 = lockAndCall(f3, f3m, nullptr);  // OK:ptr 為 std::nullptr_t
傳入實參 template 推導出的 ptr 型別 傳給 func 的結果
0 int intstd::shared_ptr<Widget>型別錯誤
NULL int 或類 int 整數型別 整數 → std::unique_ptr<Widget>型別錯誤
nullptr std::nullptr_t 隱式轉換為 Widget*編譯通過

關鍵洞見:template type deduction 會推導出 0/NULL 的「真實型別」(整數),而非其 fallback 意義(null pointer)。一旦進入 template,0/NULL 的「指標偽裝」即告失效——這是改用 nullptr 最有力的理由。

f1(0) 直接呼叫可以編譯(語境明確要求指標,0 走 fallback),但經過 template 轉手就不行——deduction 發生在「指標語境」形成之前。

Exam/Test Patterns

情境關鍵字 答案
f(0) 遇到 f(int)f(void*) overload 呼叫 f(int)0int,永不優先匹配指標版
f(NULL) 行為不確定 / 編譯失敗 NULL 可能是 0L,多重轉換同樣好 → 歧義;絕不會呼叫 f(void*)
nullptr 的型別是什麼 std::nullptr_t(定義為「nullptr 的型別」),隱式轉換為所有 raw pointer
nullptr 是指標型別嗎 不是;既非整數也非指標,但可轉換為所有指標型別
0/NULL 傳入 template 參數再轉交指標參數 編譯錯誤:deduction 得到 int/整數型別,無法轉成智慧指標或 raw pointer
nullptr 傳入 template OK:推導為 std::nullptr_t,再隱式轉換為目標指標型別
auto result = …; if (result == nullptr) 可讀性:明示 result 為指標型別,== 0 則看不出來
設計 API 時整數與指標 overload 並存 避免!0/NULL 呼叫者永遠落入整數版(C++98 守則,C++11 仍有效)