Featured image of post SFINAE

SFINAE

substitution failure is not an error

引言

​ 最近在实现vector的时候,遇到了一个问题:

1
2
template<typename InputIt>
T* insert(const T* pos, InputIt first, InputIt last)

​ 对于上面一个模板函数,如果按照上面那种写法,当调用v.insert(pos,1,2);的时候,可能也会走进上面那个函数并将InputIt推导为int类型,而实际情况是想要往pos位置上插入1个2。这就会导致编译器不知道调用哪个函数。那么应当如何解决这一个问题呢?下面就要介绍这篇文章的主人公了。

SFINAE

SFINAE(Substitution Failure Is Not An Error,替换失败不是错误)是 C++ 模板元编程中的一个重要规则。它的核心思想是:在模板实例化过程中,如果某个模板参数替换失败(例如,类型不匹配或表达式不合法),编译器不会报错,而是会忽略这个模板特化,继续尝试其他可能的模板特化或重载。


SFINAE 的核心思想

  1. 替换(Substitution)
    • 在模板实例化时,编译器会用实际的类型或值替换模板参数。
    • 例如,对于 template<typename T> void foo(T t),如果调用 foo(42),编译器会用 int 替换 T
  2. 替换失败(Substitution Failure)
    • 如果在替换过程中,某个表达式或类型不合法(例如,类型没有某个成员函数,或操作符不支持),替换就会失败。
  3. 不是错误(Not An Error)
    • 如果替换失败,编译器不会报错,而是会忽略这个模板特化,继续尝试其他可能的模板特化或重载。

SFINAE常用工具

std::enable_if

1
2
3
4
5
6
7
template<bool B, typename T = void>
struct enable_if {};

template<typename T>
struct enable_if<true, T> {
    using type = T;
};
  • 如果条件 Btruestd::enable_if<B, T> 会定义嵌套类型 type,其值为 T
  • 如果条件 Bfalsestd::enable_if<B, T> 不会定义嵌套类型 type,从而导致替换失败(SFINAE)。

例如:

1
2
3
4
template<typename T, typename std::enable_if<std::is_integral<T>::value, int>::type = 0>
void foo(T t) {
    // 只有 T 是整数类型时,这个函数才会启用
}

std::void_t

std::void_t 的核心思想是利用模板的替换规则:

  1. 如果传递给 std::void_t 的类型或表达式是合法的,那么 std::void_t 会生成 void 类型。
  2. 如果传递给 std::void_t 的类型或表达式是非法的(例如,某个类型不存在或某个操作不支持),那么模板替换会失败,触发 SFINAE,编译器会忽略这个模板特化,而不会报错。

std::declval

std::declval 的定义如下:

1
2
template<typename T>
std::add_rvalue_reference_t<T> declval() noexcept;
  • std::add_rvalue_reference_t<T> 是类型特征(type trait),用于将类型 T 转换为右值引用 T&&
  • noexcept 表示该函数不会抛出异常。

std::declval 的主要作用是在编译时生成一个类型的右值引用,而不需要实际构造该类型的对象。这在以下场景中非常有用:

  1. 类型推导:在模板元编程中,推导某个表达式的类型。
  2. SFINAE:检查某个类型是否支持特定操作(例如成员函数、操作符等)。
  3. 编译时表达式检查:在不实际构造对象的情况下,检查某个表达式是否合法。

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
template<typename T, typename = void>
struct has_foo : std::false_type {};

template<typename T>		//使用declval检查T类型能否调用foo()
struct has_foo<T, std::void_t<decltype(std::declval<T>().foo())>> : std::true_type {};

struct Bar {
    void foo() {}
};

struct Baz {};

static_assert(has_foo<Bar>::value, "Bar should have foo()");
static_assert(!has_foo<Baz>::value, "Baz should not have foo()");

最终实现insert

1
2
3
4
5
template< class InputIt, typename = std::void_t<
        decltype(*std::declval<InputIt>()),
        decltype(++std::declval<InputIt&>())
    >>
    T* insert(const T* pos, InputIt first, InputIt last)

通过SFINAE,只有支持*和++操作的InputIt才会走进这个函数,从而解决最初的那个问题。