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) 語言原生支援的真品
Things to Remember

  • 優先使用 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)──→ 編譯錯誤 ★ 最早、最一致
deleted functions 依慣例宣告為 public

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 就同時擋掉 doublefloat

核心概念三:刪除模板特化(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,且不能與主模板有不同存取層級