Item 11:優先使用 deleted functions 而非 private 未定義函式 (Prefer Deleted Functions to Private Undefined Ones)
Overview Table
| 比較面向 | C++98:private + 不定義 |
C++11:= delete |
|---|---|---|
| 錯誤偵測時機 | 一般 client:編譯期;member/friend:link 期才失敗 | 一律編譯期,任何程式碼都無法呼叫 |
| 適用範圍 | 只能用於 member functions | 任何函式:member、non-member、template instantiation |
| 宣告位置慣例 | private: 區段 |
public: 區段(錯誤訊息較清楚) |
| 參與 overload resolution | 是(但只是存取受限) | 是(deleted 函式仍是程式的一部分) |
| 可停用特定模板特化 | 否(特化無法有不同存取層級) | 可(在 namespace scope = delete) |
| 本質 | 對「禁止呼叫」的模擬 (emulation) | 語言原生支援的真品 |
- 優先使用 deleted functions,而非 C++98 的「private 且不定義」手法。
- 任何函式都可以被 delete,包括 non-member functions 與 template instantiations。
核心概念一:C++98 舊法 vs C++11 = delete
想禁止 client 呼叫編譯器自動生成的 special member functions(copy constructor、copy assignment 等,詳見 Item 17),C++98 的慣用法是「宣告為 private 且不提供定義」,例如標準庫的 basic_ios(istream/ostream 皆繼承自它,複製 stream 語義不明,故禁止複製):
// C++98 寫法:private 且不定義
template <class charT, class traits = char_traits<charT>>
class basic_ios : public ios_base {
private:
basic_ios(const basic_ios&); // 只宣告、不定義
basic_ios& operator=(const basic_ios&); // 同上
};
// C++11 寫法:public + = delete
template <class charT, class traits = char_traits<charT>>
class basic_ios : public ios_base {
public:
basic_ios(const basic_ios&) = delete; // 標記為 deleted
basic_ios& operator=(const basic_ios&) = delete; // 任何使用都是編譯錯誤
};
兩者錯誤偵測時機差異:
呼叫被禁止的 copy 操作
│
├── C++98:private + 不定義
│ ├─ 一般 client 呼叫 ──→ 編譯錯誤(存取違規)
│ └─ member / friend 呼叫 ─→ 編譯通過…
│ └──→ link 錯誤(找不到定義)★ 太晚!
│
└── C++11:= delete
└─ 任何人呼叫(含 member/friend)──→ 編譯錯誤 ★ 最早、最一致
C++ 檢查順序是「先存取權限、後 deleted 狀態」。若把 deleted 函式放在 private:,部分編譯器只會回報「函式是 private」,模糊了「函式根本不可用」的真正原因。改寫 legacy code 時把它們移到 public:,錯誤訊息通常更清楚。
核心概念二:任何函式都可 delete——過濾不想要的隱含轉換
private 只能套在 member functions 上;= delete 則可套用於任何函式,包括 non-member functions。這讓它能夠刪除特定 overloads 來擋掉不合理的隱含轉換:
bool isLucky(int number); // 原始函式:幸運數字必須是整數
bool isLucky(char) = delete; // 拒絕 char
bool isLucky(bool) = delete; // 拒絕 bool
bool isLucky(double) = delete; // 拒絕 double 與 float
if (isLucky('a')) ... // 錯誤!呼叫 deleted 函式
if (isLucky(true)) ... // 錯誤!
if (isLucky(3.5f)) ... // 錯誤!
關鍵機制:deleted 函式雖不可呼叫,但仍是程式的一部分,因此照常參與 overload resolution——先被選中、再因 deleted 而使編譯失敗。
isLucky(3.5f)(float)也被 double 版擋下?
面對 float,C++ 在「轉成 int」與「轉成 double」之間優先選擇 double(提升轉換優於整數轉換)。所以 float 引數會匹配到 isLucky(double) 這個 deleted overload,而非 int 版本——刪一個 double overload 就同時擋掉 double 和 float。
核心概念三:刪除模板特化(private 做不到的事)
= delete 能停用特定 template instantiations。例如處理原生指標的模板,想拒絕兩個特例——void*(無法解參考、遞增遞減)與 char*(通常代表 C-style 字串而非單一字元):
template<typename T>
void processPointer(T* ptr); // 主模板
template<>
void processPointer<void>(void*) = delete; // 拒絕 void*
template<>
void processPointer<char>(char*) = delete; // 拒絕 char*
若 void* / char* 不合法,const void*、const char* 通常也該刪;要徹底的話還有 const volatile 版本,以及其他字元型別的指標:std::wchar_t*、std::char16_t*、std::char32_t*。
C++98 的 private 手法在此完全失效:member function template 的特化無法擁有與主模板不同的存取層級,且特化必須寫在 namespace scope(不能寫在 class scope 內):
class Widget {
public:
template<typename T>
void processPointer(T* ptr) { /* ... */ }
private:
template<> // 錯誤!特化不能放 class scope、
void processPointer<void>(void*); // 也不能改變存取層級
};
// C++11:deleted 函式不需要不同存取層級,直接在 namespace scope 刪除
template<>
void Widget::processPointer<void>(void*) = delete; // 仍是 public,但已 deleted
結論:C++98 的 private-未定義手法「class 外不能用、class 內不一定能用、能用時也可能拖到 link 期才報錯」——一律改用 deleted functions。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
| 禁止複製類別物件,C++11 首選做法 | copy ctor / copy assignment 宣告 = delete(而非 private 不定義) |
| member/friend 誤用被禁函式,錯誤何時發現 | C++98 private 法 → link 期;= delete → 編譯期 |
| deleted 函式該放 public 還是 private | public——存取權限先於 deleted 狀態被檢查,public 給出更準確的錯誤訊息 |
| 非 member function 想擋隱含轉換 | 建立 deleted overloads(private 只能用於 member functions) |
isLucky(3.5f) 為何報錯 |
float 優先轉 double,匹配到 deleted 的 double overload |
| deleted 函式是否參與 overload resolution | 是,它仍是程式的一部分,被選中即編譯錯誤 |
想停用模板對 void* / char* 的實體化 |
在 namespace scope 對該特化 = delete;private 法辦不到 |
| 為何 private 法無法停用 member template 特化 | 特化必須在 namespace scope,且不能與主模板有不同存取層級 |
Related Notes
- 04-Moving-To-Modern-Cpp/11-Special-Member-Function-Generation:Item 17 詳解編譯器自動生成的 special member functions——
= delete正是控制其生成/停用的工具。 - 04-Moving-To-Modern-Cpp/06-Override:同章下一個 Item,
override與= delete同為 C++11 讓「意圖明確化、錯誤提早化」的宣告手段。 - 05-Smart-Pointers/01-Unique-Ptr:
std::unique_ptr的獨占語義即透過 deleted copy operations 實現,是本 Item 的標準庫實例。 - 06-Move-Semantics-And-Perfect-Forwarding/04-Overloading-On-Universal-References:同樣以 overload resolution 為核心的陷阱主題,理解 deleted overloads 參與決議是其基礎。