Item 27:熟悉在 universal reference 上重載的替代方案 (Familiarize yourself with alternatives to overloading on universal references)
Overview Table
Item 26 說明「在 universal reference 上重載」幾乎必出事;本 Item 提供 5 種替代方案:
| 方案 | 支援 perfect forwarding | 保留重載 | 適用場合 | 主要缺點 |
|---|---|---|---|---|
| 放棄重載(改用不同函式名) | 可 | 否 | 一般自由函式 | 建構子名稱固定,無法用於 constructor |
Pass by const T&(回到 C++98) |
否 | 可 | 追求簡單勝於效率 | 犧牲部分效率(多一次複製) |
| Pass by value | 否 | 可 | 參數一定會被複製時(Item 41) | 對不可低成本 move 的型別不划算 |
| Tag dispatch | 可 | 可 | 有單一未重載入口函式的一般函式 | 對 constructor 失效(編譯器自動生成的 copy/move ctor 會繞過) |
std::enable_if 約束模板 |
可 | 可 | constructor 等無法避免重載之處 | 語法繁瑣、錯誤訊息差 |
決策核心:是否需要 perfect forwarding。不需要 → 前三招(為每個參數指定具體型別);需要 → tag dispatch 或
std::enable_if。能用前三招時應優先使用。不使用 universal reference 的三種簡單方案
以 Item 26 的 Person 為例,pass by value 常能在不增加複雜度下換得效能(詳見 Item 41):
class Person {
public:
explicit Personstring n // 取代 T&& 建構子;
: namemove(n) {} // 傳值後用 std::move 移入成員
explicit Person(int idx) // 整數版重載照舊
: name(nameFromIdx(idx)) {}
private:
std::string name;
};
- 放棄重載:
logAndAdd拆成logAndAddName與logAndAddNameIdx——但建構子名稱由語言固定,此招對 ctor 無效。 const T&:最簡單、行為可預期,代價是效率不如轉發;知道 universal reference 重載的凶險後,這筆交易比想像中划算。- pass by value:
int、short、long、std::size_t等一律走int版;std::string與可轉成它的引數(如字面值"Ruth")走std::string版,呼叫端不會有意外。
例外:以
0 或 NULL 表示空指標時仍會呼叫 int 版重載——這是使用 0/NULL 的老問題,解法是永遠用 nullptr(Item 8)。Tag Dispatch:用標籤參數操控重載決議
需要 perfect forwarding 又想保留重載時,讓未重載的入口函式把工作分派給帶 tag 參數的實作重載。universal reference 參數對任何引數都是 exact match,但參數列中其他非 universal reference 參數若匹配極差,即可把該重載踢出候選——tag(std::true_type / std::false_type)正是被設計來決定勝負的參數:
template<typename T>
void logAndAdd(T&& name) // 入口:未重載,來者不拒
{
logAndAddImpl(
std::forward<T>(name),
std::is_integral<std::remove_reference_t<T>>() // 產生 tag 物件
);
}
template<typename T> // 非整數版:
void logAndAddImpl(T&& name, std::false_type) // 加入資料結構
{
auto now = std::chrono::system_clock::now();
log(now, "logAndAdd");
names.emplaceforward<T>(name);
}
std::string nameFromIdx(int idx); // 由索引查名字
void logAndAddImpl(int idx, std::true_type) // 整數版:查名字後
{ logAndAdd(nameFromIdx(idx)); } // 回頭呼叫 logAndAdd
呼叫端引數(任何型別)
│
▼
logAndAdd(T&&) ←── 單一入口,「不」重載
│ 以 std::is_integral<remove_reference_t<T>> 產生 tag
├── tag = std::false_type ──▶ logAndAddImpl(T&&, false_type)
│ (記錄 + emplace 轉發)
└── tag = std::true_type ──▶ logAndAddImpl(int, true_type)
(nameFromIdx 查表 → 再呼叫 logAndAdd)
true/false是執行期值,而重載決議是編譯期行為,故需std::true_type/std::false_type這種「型別化的布林」。- tag 參數不需命名:僅存在於編譯期以引導重載決議,執行期可望被編譯器最佳化移除。此手法是 template metaprogramming 的標準建材,現代 C++ 函式庫中隨處可見。
兩個陷阱:
- 直接寫
std::is_integral<T>是錯的——lvalue 引數會使 T 被推導為int&這類 lvalue reference(Item 28),而 reference 不是整數型別,判斷永遠為 false;必須先以std::remove_reference(C++14:std::remove_reference_t)剝除。 - tag dispatch 對 constructor 失效:編譯器自動生成的 copy/move constructor(Item 17)會繞過你的單一入口,且 universal reference ctor 仍會搶走「複製 non-const lvalue」等呼叫。
std::enable_if:約束模板的啟用條件
當 universal reference 重載「太貪心、卻又不夠貪心到能當唯一入口」(典型即 perfect-forwarding constructor),改用 std::enable_if 有條件地停用模板——條件不滿足時編譯器視同該模板不存在(背後技術是 SFINAE)。條件需層層修正:
| 演進 | 條件 | 修正原因 |
|---|---|---|
| 第 1 版 | !std::is_same<Person, T>::value |
lvalue 使 T 推導為 Person&,與 Person 不同型 → 判斷失效 |
| 第 2 版 | !std::is_same<Person, std::decay_t<T>>::value |
用 std::decay 剝除 reference 與 cv 限定,統一比較基準 |
| 第 3 版 | !std::is_base_of<Person, std::decay_t<T>>::value |
衍生類 SpecialPerson 以慣用寫法實作 copy/move ctor 時,傳給基底的引數 decay 後仍非 Person,轉發 ctor 被啟用且 exact match 勝過 derived-to-base 轉換;is_base_of 連「自己或其衍生類」一併排除(std::is_base_of<T, T>::value 為 true) |
| 最終版 | 再加 && !std::is_integral<std::remove_reference_t<T>>::value |
讓整數引數改走 Person(int) 重載,達成原始目標 |
class Person { // C++14 最終版
public:
template<
typename T,
typename = std::enable_if_t<
!std::is_base_of<Person, std::decay_t<T>>::value
&&
!std::is_integral<std::remove_reference_t<T>>::value
>
>
explicit Person(T&& n) // 給 std::string 及可轉換型別
: nameforward<T>(n)
{
static_assert( // 自訂錯誤訊息,改善可讀性
std::is_constructible<std::string, T>::value,
"Parameter n can't be used to construct a std::string");
}
explicit Person(int idx) // 整數引數專用
: name(nameFromIdx(idx)) {}
… // copy/move ctor 等
private:
std::string name;
};
如此一來,用另一個 Person(不論 lvalue/rvalue、const/非 const、volatile 與否)或其衍生類物件建構時,絕不會呼叫到 universal reference 建構子。C++14 可用 std::enable_if_t / std::decay_t 省去 typename ... ::type 樣板碼(Item 9)。
std::decay 除了剝除 reference 與 cv 限定,還會把陣列與函式型別退化為指標(同 Item 1 的 by-value 推導);在本例無妨,但別把它當成「只去 reference/cv」的 trait。Trade-offs:完美轉發的效率 vs 可用性
前三招為每個參數指定型別;後兩招用 perfect forwarding 不指定型別——這是根本分歧:
| 面向 | 指定型別(前三招) | Perfect forwarding(後兩招) |
|---|---|---|
| 效率 | 可能為符合參數型別建立暫時物件(如 "Nancy" → 暫時 std::string) |
通常較高,字面值可直達最終建構子,免暫時物件 |
| 可轉發性 | 一般引數皆可傳入 | 存在無法完美轉發的引數(Item 30) |
| 錯誤訊息 | 直白(「無法從 const char16_t[12] 轉成 int 或 std::string」) |
錯誤延遲到轉發鏈深處才爆發,可能長達 160+ 行;轉發層數越多越難懂 |
- 補救:已知轉發目標時,用
static_assertstring, T>::value, "..."給出可讀的錯誤訊息。 - 許多開發者因錯誤訊息問題,只在效能至上的介面使用 universal reference 參數。
static_assert 位於建構子本體,但轉發發生在成員初始化列表(先於本體執行),因此編譯器常先吐出上百行原始錯誤,友善訊息才姍姍來遲。Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
| 想避免 universal reference 重載、且函式不是 constructor | 改名(放棄重載)、const T&、pass by value、tag dispatch 皆可 |
| 需要 perfect forwarding + 重載,且有單一入口函式 | Tag dispatch(std::true_type/std::false_type 引導重載決議) |
tag dispatch 中 std::is_integral<T> 對 lvalue 失效 |
T 被推導為 lvalue reference → 先 std::remove_reference_t<T> |
| perfect-forwarding constructor 搶走 copy/move 呼叫 | tag dispatch 無效(編譯器生成的 SMF 繞過入口),改用 std::enable_if 停用模板 |
enable_if 用 is_same 仍擋不住 Person&、const Person |
條件中先 std::decay_t<T> 再比較 |
| 衍生類 copy/move 仍呼叫基底轉發 ctor | std::is_same 換成 std::is_base_of(is_base_of<T,T> 為 true,自身也涵蓋) |
讓 enable_if 背後生效的語言機制 |
SFINAE(替換失敗不是錯誤,模板被移出候選集) |
| 完美轉發介面的兩大可用性缺點 | 部分引數無法轉發(Item 30);錯誤訊息冗長難懂 |
| 想給轉發建構子友善的編譯錯誤 | static_assert + std::is_constructible |
| 傳值後把參數移入成員的慣用寫法 | explicit Personstring n) : name(std::move(n)(Item 41) |
Related Notes
- 06-Move-Semantics-And-Perfect-Forwarding/04-Overloading-On-Universal-References — Item 26:本 Item 所解決的問題來源(貪婪的 universal reference 重載)
- 06-Move-Semantics-And-Perfect-Forwarding/02-Universal-References — Item 24:universal reference 的定義與判別
- 06-Move-Semantics-And-Perfect-Forwarding/06-Reference-Collapsing — Item 28:lvalue 使 T 推導為 lvalue reference 的機制(
remove_reference/decay之必要性) - 06-Move-Semantics-And-Perfect-Forwarding/08-Perfect-Forwarding-Failure-Cases — Item 30:無法完美轉發的引數種類
- 09-Tweaks/01-Pass-By-Value — Item 41:pass by value 方案的成本分析
- 04-Moving-To-Modern-Cpp/03-Alias-Declarations — Item 9:
std::enable_if_t、std::decay_t等 alias template 與 type traits - 04-Moving-To-Modern-Cpp/02-Nullptr — Item 8:
0/NULL落入int重載的陷阱 - 04-Moving-To-Modern-Cpp/11-Special-Member-Function-Generation — Item 17:編譯器自動生成 copy/move ctor 的規則(tag dispatch 被繞過的原因)