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;
};
例外:以 0NULL 表示空指標時仍會呼叫 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)
兩個陷阱:

  1. 直接寫 std::is_integral<T> 是錯的——lvalue 引數會使 T 被推導為 int& 這類 lvalue reference(Item 28),而 reference 不是整數型別,判斷永遠為 false;必須先以 std::remove_reference(C++14:std::remove_reference_t)剝除。
  2. 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] 轉成 intstd::string」) 錯誤延遲到轉發鏈深處才爆發,可能長達 160+ 行;轉發層數越多越難懂
static_assert 位於建構子本體,但轉發發生在成員初始化列表(先於本體執行),因此編譯器常吐出上百行原始錯誤,友善訊息才姍姍來遲。

Exam/Test Patterns

情境關鍵字 答案
想避免 universal reference 重載、且函式不是 constructor 改名(放棄重載)、const T&、pass by value、tag dispatch 皆可
需要 perfect forwarding + 重載,且有單一入口函式 Tag dispatchstd::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_ifis_same 仍擋不住 Person&const Person 條件中先 std::decay_t<T> 再比較
衍生類 copy/move 仍呼叫基底轉發 ctor std::is_same 換成 std::is_base_ofis_base_of<T,T> 為 true,自身也涵蓋)
enable_if 背後生效的語言機制 SFINAE(替換失敗不是錯誤,模板被移出候選集)
完美轉發介面的兩大可用性缺點 部分引數無法轉發(Item 30);錯誤訊息冗長難懂
想給轉發建構子友善的編譯錯誤 static_assert + std::is_constructible
傳值後把參數移入成員的慣用寫法 explicit Personstring n) : name(std::move(n)(Item 41)