Item 26:避免對 universal reference 進行重載 (Avoid Overloading on Universal References)

Overview Table

重點 說明
核心禁令 不要把 universal reference 函式與其他重載放在一起——它幾乎總是被呼叫得比預期更頻繁
原因 universal reference 函式是 C++ 中最貪婪 (greediest) 的函式:模板實例化能對幾乎任何型別產生 exact match,勝過需要轉型/提升的重載
最危險場景 perfect-forwarding constructor:對 non-const lvalue 比 copy constructor 更佳匹配;還會攔截 (hijack) derived class 對 base class copy/move constructor 的呼叫
典型症狀 傳入 shortstd::size_t 等非 int 整數型別 → 走進 universal reference 版本 → 深層編譯錯誤
解法 見 Item 27 的替代方案(tag dispatch、std::enable_if 約束等)

動機:logAndAdd 從 lvalue-ref-to-const 到 universal reference

const std::string& 接收參數在三種呼叫情境下都至少付一次 copy;改用 universal reference + std::forward 可達最佳效率:

std::multiset<std::string> names;        // 全域資料結構

template<typename T>
void logAndAdd(T&& name)                 // universal reference(Item 24)
{
  auto now = std::chrono::system_clock::now();
  log(now, "logAndAdd");
  names.emplaceforward<T>(name);  // 依 Item 25 用 std::forward 轉發
}

std::string petName("Darla");
logAndAdd(petName);                      // lvalue:仍是 copy(無法避免)
logAndAddstring("Persephone");    // rvalue:move 而非 copy
logAndAdd("Patty Dog");                  // 字面值:直接在 multiset 內建構
                                         // std::string,連 move 都省了
呼叫方式 const std::string& 版本 T&& + std::forward 版本
傳 lvalue std::string copy copy(本來就無法避免)
傳 rvalue std::string copy move
傳字串字面值 產生暫時物件再 copy 原地建構,零暫時物件

加入重載後的災難:exact match 勝過 promotion

為支援用索引查名字的客戶端而加上 logAndAdd(int idx) 重載後,麻煩開始了:

std::string nameFromIdx(int idx);        // 依索引回傳名字

void logAndAdd(int idx)                  // 新重載
{
  auto now = std::chrono::system_clock::now();
  log(now, "logAndAdd");
  names.emplace(nameFromIdx(idx));
}

logAndAdd(22);                           // OK:呼叫 int 重載

short nameIdx = 22;
logAndAdd(nameIdx);                      // 編譯錯誤!
呼叫 logAndAdd(short)
        │
        ├─ template<T> logAndAdd(T&&):T 推導為 short& → exact match ★勝出
        │        │
        │        └─ names.emplace(short) → std::string 沒有接受 short
        │           的建構子 → 深層編譯錯誤
        │
        └─ logAndAdd(int):short → int 需要 promotion → 落選
重載決議規則:exact match 勝過需要 promotion 的匹配。universal reference 模板能對幾乎任何實參型別實例化出 exact match,因此會「吸走」遠多於開發者預期的呼叫。
「幾乎任何型別」有例外:少數實參種類無法被 universal reference 完美吸收(如 braced initializer、0/NULL 作為 null pointer 等),詳見 06-Move-Semantics-And-Perfect-Forwarding/08-Perfect-Forwarding-Failure-Cases(Item 30)。

Perfect-forwarding constructor:問題加倍嚴重

把同樣邏輯搬進類別建構子時,還會與編譯器生成的 copy/move constructor(Item 17)互相干擾:

class Person {
public:
  template<typename T>
  explicit Person(T&& n)                 // perfect-forwarding ctor
  : nameforward<T>(n) {}

  explicit Person(int idx)               // int ctor
  : name(nameFromIdx(idx)) {}

  Person(const Person& rhs);             // copy ctor(編譯器生成)
  Person(Person&& rhs);                  // move ctor(編譯器生成)
private:
  std::string name;
};

Person p("Nancy");
auto cloneOfP(p);                        // 想 copy —— 但編譯失敗!
被複製物件 匹配情形 實際呼叫
non-const lvalue p 模板實例化出 Person(Person&),exact match;copy ctor 需加 const forwarding ctor(用 Person 初始化 std::string → 編譯錯誤)
const lvalue cp 模板可實例化出相同簽名 Person(const Person&),與 copy ctor 平手 copy ctor(平手時 normal function 優先於模板實例化
繼承讓問題更隱蔽:derived class 的 copy/move constructor 以 SpecialPerson 型別的實參呼叫 base class 建構子——對 base 而言那不是 Person(需要轉換才能匹配 copy/move ctor),於是 base 的 forwarding ctor 攔截了呼叫,最終因 std::string 沒有接受 SpecialPerson 的建構子而編譯失敗。
class SpecialPerson : public Person {
public:
  SpecialPerson(const SpecialPerson& rhs)  // copy ctor
  : Person(rhs) { /* … */ }                // 呼叫的是 base 的 forwarding ctor!

  SpecialPerson(SpecialPerson&& rhs)       // move ctor
  : Personmove(rhs) { /* … */ }     // 同樣呼叫 forwarding ctor!
};
即使類別含有可實例化為 copy/move constructor 簽名的模板建構子,編譯器仍會照 Item 17 的規則生成 copy/move constructor——模板建構子不會抑制它們的生成,兩者並存才造成上述重載混戰。
需要「大多數型別走轉發、少數型別特殊處理」時,不要用重載硬拚——改用 Item 27 的替代方案(放棄重載、pass by value、tag dispatch、std::enable_if 約束模板)。

Exam/Test Patterns

情境關鍵字 答案
T&& 重載 + int 重載,傳入 short 走 universal reference 版本:exact match 勝過 promotion → 常導致深層編譯錯誤
「universal reference 函式為何危險」 是 C++ 最貪婪的函式,幾乎對任何型別實例化出 exact match,吸走預期外的呼叫
auto cloneOfP(p); 不呼叫 copy ctor」 p 為 non-const lvalue → forwarding ctor 實例化出 Person(Person&) 是更佳匹配
「const 物件複製卻正常」 模板與 copy ctor 平手時,normal function 優先於模板實例化
「derived copy/move ctor 編譯失敗」 傳給 base 的是 SpecialPerson,被 base 的 forwarding ctor 攔截(hijack)
「有模板建構子,編譯器還生成 copy/move ctor 嗎」 會,模板建構子不算 copy/move ctor,不抑制生成(Item 17)
「該怎麼辦」 避免對 universal reference 重載;改用 Item 27 的替代方案