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 的呼叫 |
| 典型症狀 | 傳入 short、std::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 的替代方案 |
Related Notes
- 06-Move-Semantics-And-Perfect-Forwarding/02-Universal-References — Item 24:如何辨識 universal reference,本 Item 的前置知識
- 06-Move-Semantics-And-Perfect-Forwarding/03-Move-Vs-Forward-Usage — Item 25:universal reference 應搭配
std::forward轉發 - 06-Move-Semantics-And-Perfect-Forwarding/05-Alternatives-To-Overloading — Item 27:本 Item 問題的完整解法集(tag dispatch、
std::enable_if等) - 06-Move-Semantics-And-Perfect-Forwarding/08-Perfect-Forwarding-Failure-Cases — Item 30:universal reference 無法完美吸收的少數實參種類
- 04-Moving-To-Modern-Cpp/11-Special-Member-Function-Generation — Item 17:編譯器何時生成 copy/move constructor,理解 forwarding ctor 衝突的關鍵
- 09-Tweaks/02-Emplacement — Item 42:
emplace原地建構,logAndAdd 效率範例的基礎