Item 8:優先使用 nullptr 而非 0 與 NULL (Prefer nullptr to 0 and NULL)
Overview Table
| 主題 | 重點 |
|---|---|
| 核心建議 | 想表達 null pointer 時一律用 nullptr,不要用 0 或 NULL |
0 的本質 |
0 是 int,不是指標;只有在「非指標不可」的語境才勉強被解讀為 null pointer |
NULL 的本質 |
實作可定義為任意整數型別(0、0L…),同樣不是指標型別 |
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++ 的基本原則:0 是 int。只有當語境要求指標時,編譯器才退而求其次把 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 依然成立,因為總有人繼續寫 0 和 NULL。
0 / NULL ──(本質是整數)──▶ 優先匹配 f(int)/f(bool) ✘ 意外
│
└─(僅 fallback)─▶ null pointer
nullptr ──nullptr_t)──▶ 隱式轉換到所有指標型別 ──▶ f(void* ✔
│
└─✘ 無法轉換為整數型別
例外與細節
nullptr嚴格來說也不是指標型別,其真正型別是std::nullptr_t;因為它能隱式轉換為所有 raw pointer 型別,才「表現得像所有型別的指標」。NULL的確切型別依實作而異(0、0L等),因此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 |
int → std::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);0 是 int,永不優先匹配指標版 |
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 仍有效) |
Related Notes
- 04-Moving-To-Modern-Cpp/01-Braced-Initialization —— 同章前一則:另一個「語法選擇影響呼叫結果」的主題
- 04-Moving-To-Modern-Cpp/03-Alias-Declarations —— 範例中
MuxGuard的 alias declaration 寫法(Item 9) - 02-Deducing-Types/01-Template-Type-Deduction —— template type deduction 為何把
0/NULL推導成int(Item 1) - 02-Deducing-Types/03-Decltype ——
lockAndCall回傳型別decltype(auto)與尾置回傳型別的原理(Item 3) - 04-Moving-To-Modern-Cpp/Practice-Moving-To-Modern-Cpp —— 本章練習題