Skip to content

SFINAE 与 enable_if / void_t 探测惯用法

一、SFINAE 是什么(先讲清楚字面)

Substitution Failure Is Not An Error(替换失败不是错误): 在模板实例化进行参数替换(substitution)时,若因替换导致某处病式(ill-formed)——如不存在的嵌套类型、调不匹配——不直接报编译错,而是把该候选从重载集合中“安静”剔除,继续看其它可匹配候选。只有全部候选都失败才真正报错。

cpp
template <typename T>
auto size(T& c) -> decltype(c.size(), void()) { return c.size(); }  // 有 .size() 才可行

二、std::enable_if 惯用法(让重载按特性分派)

cpp
// 只对整型可用
template <typename T>
std::enable_if_t<std::is_integral_v<T>, T> twice(T x) { return x*2; }

// 重载两版按“是否算术”选择(避免同时候选报错)
template <typename T>
std::enable_if_t<std::is_integral_v<T>, void> f(T x);   // 整型版
template <typename T>
std::enable_if_t<!std::is_integral_v<T>, void> f(T x);  // 其它类型版

enable_if 真值

cpp
template <bool B, typename T=void> struct enable_if {};   // 主模板:无 type
template <typename T>               struct enable_if<true,T>{ using type=T; }; // 特化 true
  • 条件 false → 主模板没有 ::type → 用到 enable_if_t<false> 处替换失败 → 该候选被剔除(SFINAE)。
  • 因此 enable_if 能做「按 trait 开关某重载」。

三、函数返回类型 vs 参数 vs 模板默认参——三种放法

  1. 返回类型 std::enable_if_t<cond,T> f()
  2. 额外默认模板参 template<typename T, std::enable_if_t<cond,int> =0> void f()
  3. 形参(少见)

工程里默认模板参数版最常见(返回类型被 enable_if 污染稍不雅)。

四、void_t “探测是否存在成员/操作”惯用法(C++ 风格高档技法)

cpp
template <typename...> using void_t = void;   // 任何 type-list 都映射成 void

// 探测是否有 size() 方法
template <typename T, typename = void>
struct has_size : std::false_type {};
template <typename T>
struct has_size<T, void_t<decltype(std::declval<T>().size())>> : std::true_type {};
//                    ^SFINAE: 若 T.size() 不可用,该特化替换失败→回退主模板 false

static_assert(has_size<std::string>::value);  // true

解释给面试官:用 void_t 把「某表达式是否合法」装进模板默认实参;合法则特化命中,否则 SFINAE 剔除取 false。此技法即“表达式合法性探测”,是 concepts 的旧实现基础。

配套常用:std::declval<T>()——不求值地制造一个 T 的引用,用于在 decltype 里探测表达式在“不真正构造对象”的情形下是否合法。必须在未求值上下文(decltype/sizeof)用。

五、C++17 起 SFINAE 的大半被替代:if constexpr

SFINAE/enable_if 的许多用途(“对整型做 A、其它做 B”)用 if constexpr 写更清晰:

cpp
template <typename T> void clear(T& c) {
    if constexpr (std::is_trivially_destructible_v<T>) /* 直接重置 */;
    else                                             /* 逐项 clear */;
}

(真实更常见于迭代不可平凡析构的类型。)注意 if constexpr 作用在当前函数模板的单一行,不像 enable_if 在“候选重载层面”剔选项——两者互补:要“换整个重载/返回类型”用 enable_if/constraints,要在函数体内剪枝用 if constexpr。

六、C++20 concepts:终结 SFINAE 可读性问题

constraints (requires) 做“可读的布尔接口”,诊断信息好于 enable_if(不再“no matching function”模糊报错)。见 concepts.md。面试可答:老代码见 SFINAE,新代码倾向 concepts,但理解 SFINAE 才能读懂大量现存库(Boost/旧工程)。

七、易踩

  1. SFINAE 只对替换失败起作用;如果实例化后函数体内(非签名)编译错误仍会报错。
  2. declval 只能 未求值上下文。
  3. 不要四处叠 enable_if 造成可读性灾难——能用 if constexpr/overload 就用。
  4. 判断 SFINAE 是否起效注意重载候选剔除顺序;当错误在“更专业的候选被剔后主模板没法调用”才会最终报错。

C++ 面试八股 · VitePress 版