C++模板编程 | `std::enable_if` 和 SFINAE

1. std::enable_if 是什么

std::enable_if 是一个模板类:

  • 它是 C++11 起提供的
  • 它定义为 struct, 我们把 struct 和 class 一样看待
  • 它定义的时候还有模板参数, 因此它是模板类

在 GCC11 (ubuntu 22.04) 下跳转定义,看到它的定义很简单:

/usr/include/c++/11/type_traits:

  /// Define a member typedef `type` only if a boolean constant is true.
  template<bool, typename _Tp = void>
    struct enable_if
    { };

  // Partial specialization for true.
  template<typename _Tp>
    struct enable_if<true, _Tp>
    { typedef _Tp type; };


  /// Alias template for enable_if
  template<bool _Cond, typename _Tp = void>
    using enable_if_t = typename enable_if<_Cond, _Tp>::type;

std::enable_if<>是一种类型萃取(type trait),会根据给定的一个编译时期的表达式(第一个参数)来确定其行为:

如果这个表达式为true,std::enable_if<>::type会返回:
如果没有第二个模板参数,返回类型是void。
否则,返回类型是其第二个参数的类型。

如果表达式结果false,std::enable_if<>::type不会被定义。根据下面会介绍的SFINAE(substitute failure is not an error), 这会导致包含std::enable_if<>的模板被忽略掉。

2. std::enable_if 直观例子

定义一个模板函数 foo, 只能接受整数参数。 如果传入浮点类型参数, 在编译阶段就报错。

#include <type_traits>
#include <stdio.h>

template<typename T, typename = std::enable_if_t<std::is_integral<T>::value>>
void foo(T value) {
    printf("integral\n");
}

int main()
{
    foo(1); // ok
    //foo(2.3); // 编译报错
    foo<int>(1); // ok
    foo<int, int>(1); // ok
    foo<int, float>(1); // ok
    foo<int, float>(2.3); // ok, 编译能通过,覆盖了第二个模板参数,但是不推荐这么写
    return 0;
}

foo 函数是模板函数, 有两个模板参数:

  • 当没有传入两个模板参数时
    • 当没有传入模板参数T时,会从传入的参数value进行匹配
    • 当只传入一个模板参数时,通常是和第一个参数类型一样的类型
    • 此时会根据类型 T, 在编译期计算出第二个模板参数
      • 如果计算成功, 则第二个模板参数是 void
      • 如果 T 不是整数类型, 那么 std::is_integral<T>::value 结果为 false, std::enable_if_t<> 此时不会定义任何类型, 这会导致模板实例化失败
      • 但由于 SFINAE(Substitution Failure Is Not An Error)原则,这种失败不会被视为编译错误。相反,编译器会简单地忽略这个模板函数重载,继续查找其他可能的重载或模板实例。
      • 由于没找到 foo(double) 类型的定义, 因此编译报错了

看下面这个例子,就能理解 SFINAE 了:

#include <type_traits>
#include <stdio.h>

template<typename T, typename = std::enable_if_t<std::is_integral<T>::value>>
void foo(T value) {
    printf("integral\n");
}

void foo(double)
{
    printf("double\n");
}

int main()
{
    foo(1); // ok
    foo(2.3);  // 编译没有报错,因为 SFINAE 原则下, 编译器找到了 foo(double)
    foo<int>(1); // ok
    foo<int, int>(1); // ok
    foo<int, float>(1); // ok
    foo<int, float>(2.3); // ok, 编译能通过,覆盖了第二个模板参数,但是不推荐这么写
    return 0;
}

3. SFINAE 原则

3.1 什么是 SFINAE

SFINAE: Substitudion Failure Is Not An Error 的缩写, 意思是 “替换失败并不是错误”.

就是说,匹配重载的函数 / 类时如果没有匹配上,编译器并不会报错,相应的, 这个函数 /或类就不会作为候选。这是一个 C++11 的新特性,也是 enable_if 最核心的原理。

SFANAE 是 C++ 语言标准中固有的一部分, 是模板编程中的一个核心特性。 SFINAE 不是可以选择开启或关闭的功能, 而是编译器在处理模板时自然遵循的原则。

3.2 是什么是 Substitution (替换)?

什么是 Substitusion ? 指的是编译器尝试将模板参数(如类型或值)替换到模板定义中的过程, 这个替换过程是模板实例化的一部分,即创建具体函数或类型实例的过程。

举例:

template<typename T>
void func(T t)
{

}

当调用 func(42) 时, 编译器会将 T 替换为 int, 因为 42 是一个整数。 这个替换过程是成功的。

3.3 什么是 Substitution Failure (替换失败)?

SFANEA 的关键在于, 如果替换过程中发生了失败, 这种失败并不会导致编译错误。相反, 这个特定的模板实例会被编译器忽略, 编译器会继续寻找其他可能的模板匹配。

失败的例子可能包括:

  • 尝试访问不存在的类型成员
  • 尝试执行不支持的操作, 如对非数值类型执行数学运算
  • 用错误的类型作为模板参数

例如如下代码:

template<typename T>
typename T::type func(T t)
{
    ...
}

struct X {};

int main()
{
    X x;
    func(x);    // 这里发生替换失败,因为 X 没有名为 type 的成员
    return 0;
}

3.4 重新表述对于 SFINAE 的理解

SFINAE(Substitution Failure Is Not An Error)的机制确实意味着在模板参数匹配过程中,如果某次替换(Substitution)失败,并不会立即导致编译错误。编译器会继续尝试其他可用的模板特化或重载,寻找一个合适的匹配。只有当所有可能的选项都尝试过,且没有任何一个成功匹配时,才会导致编译失败。

在考虑模板参数匹配时,编译器的行为类似于:

  • 尝试将提供的实参替换到所有候选模板中。
  • 如果替换过程在某个模板中失败,这种失败并不会立即导致编译错误。相反,编译器会忽略这个失败的模板,继续尝试其他候选模板。
  • 如果存在至少一个替换成功的模板,编译器将使用这个模板进行编译。
  • 如果所有候选模板都尝试过,且都无法成功替换,则最终会导致编译失败。

这个机制使得模板编程具有极大的灵活性和表达力,允许开发者设计出能够根据不同条件自适应选择最合适实现的模板代码。通过利用SFINAE,可以实现诸如编译时类型检查、条件性启用模板特化或函数重载等高级编程技巧。

总的来说,SFINAE是C++模板编程中的一个基本原则,深入理解和掌握它对于编写高质量的C++代码非常重要。

References

https://yixinglu.gitlab.io/enable_if.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值