SFINAE 相對於 C++20 concepts
/ SFINAE, say 'SFEE-nay'; concepts as written /
當你寫一個泛型函式時,你常希望它只適用於支援某項操作的型別——比方說,只適用於有 .size() 方法的型別,或只適用於數值。若沒有辦法表達這點,模板會急切地試圖為任何型別編譯,然後在深處以一整面牆的錯誤失敗。SFINAE 與 C++20 concepts 都是用來說「這個模板只對滿足這些要求的型別有效」的機制——而 concepts 是那個老舊、晦澀的 SFINAE 技巧的現代、可讀替代品。
SFINAE 代表「代入失敗不是錯誤(substitution failure is not an error)」。想法是:當編譯器試圖實例化模板並把你的型別代入宣告時,若那次代入產生了無效的型別或運算式,編譯器不會引發錯誤——它只是悄悄地把那個模板從候選集合中移除,再往別處找。函式庫作者利用這點,依型別性質啟用或停用多載,通常搭配 std::enable_if,產生強大卻幾乎無法閱讀的程式碼。C++20 concepts 用對型別具名、可組合的述語取代它:你把像 Sortable 這樣的 concept 寫一次,然後用 requires 子句、或在原本寫 class 的地方用 concept 名稱來約束模板。編譯器預先檢查約束,若失敗,就明白地告訴你你的型別不滿足那個具名要求。
為何重要:約束把「兩百行模板實例化的鬼話」變成「std::string 不滿足 Number」。Concepts 也讓編譯器乾淨地選出最特殊的相符多載,並在模板簽章中記錄它的要求。誠實的細節:concepts 不會讓模板變成動態型別,也不增加執行期檢查——它們純粹是編譯期的關卡,所以在執行期毫無成本,而 SFINAE 仍是某些函式庫內部與你會遇到的較舊程式碼的底層。有選擇時 concepts 是較好的工具;SFINAE 正是它們被設計來退役的那個東西。
template<class T> requires std::integral<T> T half(T x) { return x / 2; } // half("hi") 明白地失敗:「hi」不滿足 std::integral
requires 子句把型別要求預先講清楚,於是錯誤的呼叫會以清楚訊息被拒絕,而非深層模板錯誤。
Concepts 是編譯期約束,不是執行期多型:它們在任何程式碼執行前就決定哪個模板有效,並增加零執行期成本。它們不取代虛擬函式或執行期分派——那是完全不同的機制。