C++ enable_if的用法(非常详细)
C++ 开发人员多年来一直使用称为 enable_if 的类模板实现对模板类型的约束。模板的 enable_if 家族已经成为 C++11 标准的一部分,实现方式如下:
注意,为了能够使用 std::enable_if,必须包含头文件
1) 在类模板形参上仅对满足指定条件的类型启用类模板:
2) 在函数模板形参、函数形参或函数返回类型上,仅针对满足指定条件的类型启用函数模板:
为了简化使用 std::enable_if 时编写的混乱代码,我们可以利用模板别名并定义两个别名 EnableIf 和 DisableIf:
基于这些模板别名,以下定义等价于前面的定义:
当编译器遇到函数调用时,它需要构建一组可能的重载,并根据函数调用的参数选择最佳匹配重载。在构建此重载集时,编译器也会计算函数模板,并且必须在模板实参中对指定的或推导出的类型执行替换。根据 SFINAE 规则,当替换失败时,编译器应该从重载集中删除函数模板并继续,而不是产生错误。
标准指定了类型和表达式错误的列表,这些错误也是 SFINAE 错误。其中包括:
让我们考虑 func() 函数的以下两个重载:
如果编译器遇到诸如 func<some_type<int>>(42) 这样的调用,那么它将构建一个包含 void func(some_type<int>::value_type const> 和 void func(int const) 的重载集。在这种情况下,最佳匹配重载是第一个重载,这次没有涉及 SFINAE。
如果编译器遇到像 func("string"s) 这样的调用,那么它再次依赖 SFINAE 来忽略函数模板,因为 std::basic_string 也没有 value_type 成员。然而,这一次重载集不包含字符串参数的任何匹配重载,因此程序是格式错误的,编译器会发出错误并停止。
类模板 enable_if<bool,T> 没有任何成员,但它的部分特化 enable_if<true,T> 确实有一个称为 type 的内部类型,它是 T 的同义词。当作为 enable_if 的第一个参数提供的编译时表达式计算结果为 true 时,内部成员 type 可用,否则就不可用。
考虑前文中函数 mul() 的最后一个定义,当编译器遇到像 mul(1,2) 这样的调用时,它尝试用 int 代替模板形参 T。因为 int 是一个整型,std::is_integral<T> 的计算结果为 true,所以,如果定义了称为 type 的内部类型,则 enable_if 特化被实例化。因此,模板别名 EnableIf 成为该类型的同义词,即 void(来自表达式 typename T=void),结果是一个函数模板 int mul<int, void>(int a, int b),可以使用提供的参数调用它。
当编译器遇到像 mul(1.0, 2.0) 这样的调用时,它试图用 double 替换模板形参 T。然而,这不是整型,结果 std::enable_if 中的条件计算结果为 false,并且类模板没有定义内部成员 type,这将会导致替换错误,但根据 SFINAE,编译器不会发出错误,而是继续执行。但是由于没有发现其他的重载,因此将没有可以调用的 mul() 函数,最终该程序被认为是格式错误的,编译器会因错误而停止。
类模板 pod_wrapper 也会遇到类似的情况,它有两个模板类型参数:
如果类型是 POD 类型(如 pod_wrapper<int>),则存在来自 enable_if 的内部成员 type,它将替代第二个模板类型参数。但是,如果内部成员 type 不是 POD 类型(如pod_wrapper<std::string>),则不定义内部成员 type,替换失败,产生错误,比如“模板参数太少”。
然而,static_assert 和 std::enable_if 的工作原理截然不同,并不能互为替代方案。
static_assert 不依赖于 SFINAE,在执行重载解析后应用,断言失败的结果是发出编译错误。std::enable_if 用于从重载集中删除候选对象,不会触发编译错误(假定标准为 SFINAE 指定的异常不会发生)。SFINAE 之后可能发生的实际错误是空的重载集,这会使程序格式错误。这是因为无法执行特定的函数调用。
为了理解 SFINAE 下 static_assert 和 std::enable_if 之间的区别,我们考虑希望有两个函数重载的情况:一个用于整型的形参,另一个用于除整型以外的其他类型的形参。使用 static_assert,我们可以编写以下内容(需要注意的是,第二个重载上的虚拟第二类型参数是定义两个不同的重载所必需的,否则就只会得到同一个函数的两个定义):
这个问题的解决方案是使用 std::enable_if 和 SFINAE。我们通过模板别名 EnableIf 和 DisableIf 来使用 std::enable_if(尽管我们仍然在第二个重载上使用虚拟模板参数来引入两个不同的定义)。
下面的示例显示了重写的两个重载,第一个重载仅对整型启用,而第二个重载对整型禁用:
template<bool Test, class T = void> struct enable_if {}; template<class T> struct enable_if<true, T> { typedef T type; };
注意,为了能够使用 std::enable_if,必须包含头文件
<type_traits>
。C++ enable_if的使用方式
std::enable_if 可以在多个作用域中使用,以达到不同的目的。请考虑以下例子:1) 在类模板形参上仅对满足指定条件的类型启用类模板:
template <typename T, typename = typename std::enable_if_t<std::is_standard_layout_v<T>, T>> class pod_wrapper { T value; }; struct point { int x; int y; }; pod_wrapper<int> w1; // OK pod_wrapper<point> w2; // OK pod_wrapper<std::string> w3; // error: POD type expected
2) 在函数模板形参、函数形参或函数返回类型上,仅针对满足指定条件的类型启用函数模板:
template<typename T, typename = typename std::enable_if_t<std::is_integral_v<T>, T>> auto mul(T const a, T const b) { return a * b; } auto v1 = mul(1, 2); // OK auto v2 = mul(1.0, 2.0); // error: Integral type expected
为了简化使用 std::enable_if 时编写的混乱代码,我们可以利用模板别名并定义两个别名 EnableIf 和 DisableIf:
template <typename Test, typename T = void> using EnableIf = typename std::enable_if_t<Test::value, T>; template <typename Test, typename T = void> using DisableIf = typename std::enable_if_t<!Test::value, T>;
基于这些模板别名,以下定义等价于前面的定义:
template <typename T, typename = EnableIf<std::is_standard_layout<T>>> class pod_wrapper { T value; }; template<typename T, typename = EnableIf<std::is_integral<T>>> auto mul(T const a, T const b) { return a * b; }
C++ enable_if的工作原理
std::enable_if 之所以能够正常工作,是因为编译器在执行重载解析时应用了 SFINAE(Substitution Failure Is Not An Error)规则。在解释 std::enable_if 如何工作之前,我们应该快速了解一下 SFINAE 是什么。当编译器遇到函数调用时,它需要构建一组可能的重载,并根据函数调用的参数选择最佳匹配重载。在构建此重载集时,编译器也会计算函数模板,并且必须在模板实参中对指定的或推导出的类型执行替换。根据 SFINAE 规则,当替换失败时,编译器应该从重载集中删除函数模板并继续,而不是产生错误。
标准指定了类型和表达式错误的列表,这些错误也是 SFINAE 错误。其中包括:
- 尝试创建 void 数组或大小为 0 的数组;
- 尝试创建对 void 的引用;
- 尝试使用 void 类型参数创建函数类型;
- 尝试在模板参数表达式或函数声明使用的表达式中执行无效转换。
有关异常的完整列表,请参阅 C++ 标准或其他资源。
让我们考虑 func() 函数的以下两个重载:
- 第一个重载是一个函数模板,它仅有一个类型为 T::value_type 的参数,这意味着它只能用具有名为 value_type 的内部类型的类型实例化;
- 第二个重载是一个函数,它只有一个 int 类型的参数:
template <typename T> void func(typename T::value_type const a) { std::cout << "func<>" << '\n'; } void func(int const a) { std::cout << "func" << '\n'; } template <typename T> struct some_type { using value_type = T; };如果编译器遇到像 func(42) 这样的调用,那么它必须找到一个可以接受 int 参数的重载。当它构建重载集并用提供的模板实参替换模板形参时,其结果 void func(int::value_type const) 是无效的,因为 int 没有 value_type 成员。由于 SFINAE,编译器不会发出错误并停止,而是简单地忽略重载并继续,然后它找到 void func(int const),这将是它调用的最佳(且唯一)匹配重载。
如果编译器遇到诸如 func<some_type<int>>(42) 这样的调用,那么它将构建一个包含 void func(some_type<int>::value_type const> 和 void func(int const) 的重载集。在这种情况下,最佳匹配重载是第一个重载,这次没有涉及 SFINAE。
如果编译器遇到像 func("string"s) 这样的调用,那么它再次依赖 SFINAE 来忽略函数模板,因为 std::basic_string 也没有 value_type 成员。然而,这一次重载集不包含字符串参数的任何匹配重载,因此程序是格式错误的,编译器会发出错误并停止。
类模板 enable_if<bool,T> 没有任何成员,但它的部分特化 enable_if<true,T> 确实有一个称为 type 的内部类型,它是 T 的同义词。当作为 enable_if 的第一个参数提供的编译时表达式计算结果为 true 时,内部成员 type 可用,否则就不可用。
考虑前文中函数 mul() 的最后一个定义,当编译器遇到像 mul(1,2) 这样的调用时,它尝试用 int 代替模板形参 T。因为 int 是一个整型,std::is_integral<T> 的计算结果为 true,所以,如果定义了称为 type 的内部类型,则 enable_if 特化被实例化。因此,模板别名 EnableIf 成为该类型的同义词,即 void(来自表达式 typename T=void),结果是一个函数模板 int mul<int, void>(int a, int b),可以使用提供的参数调用它。
当编译器遇到像 mul(1.0, 2.0) 这样的调用时,它试图用 double 替换模板形参 T。然而,这不是整型,结果 std::enable_if 中的条件计算结果为 false,并且类模板没有定义内部成员 type,这将会导致替换错误,但根据 SFINAE,编译器不会发出错误,而是继续执行。但是由于没有发现其他的重载,因此将没有可以调用的 mul() 函数,最终该程序被认为是格式错误的,编译器会因错误而停止。
类模板 pod_wrapper 也会遇到类似的情况,它有两个模板类型参数:
- 第一个是被包装的实际 POD 类型;
- 第二个是 enable_if 和 is_pod 替换的结果。
如果类型是 POD 类型(如 pod_wrapper<int>),则存在来自 enable_if 的内部成员 type,它将替代第二个模板类型参数。但是,如果内部成员 type 不是 POD 类型(如pod_wrapper<std::string>),则不定义内部成员 type,替换失败,产生错误,比如“模板参数太少”。
C++ static_assert和std::enable_if的对比
static_assert 和 std::enable_if 可以实现相同的目标。实际上,我们定义了相同的类模板 pod_wrapper 和函数模板 mul()。对于这些示例,static_assert 似乎是更好的解决方案,因为编译器会发出更好的错误消息(前提是在 static_assert 声明中指定了相关消息)。然而,static_assert 和 std::enable_if 的工作原理截然不同,并不能互为替代方案。
static_assert 不依赖于 SFINAE,在执行重载解析后应用,断言失败的结果是发出编译错误。std::enable_if 用于从重载集中删除候选对象,不会触发编译错误(假定标准为 SFINAE 指定的异常不会发生)。SFINAE 之后可能发生的实际错误是空的重载集,这会使程序格式错误。这是因为无法执行特定的函数调用。
为了理解 SFINAE 下 static_assert 和 std::enable_if 之间的区别,我们考虑希望有两个函数重载的情况:一个用于整型的形参,另一个用于除整型以外的其他类型的形参。使用 static_assert,我们可以编写以下内容(需要注意的是,第二个重载上的虚拟第二类型参数是定义两个不同的重载所必需的,否则就只会得到同一个函数的两个定义):
template <typename T> auto compute(T const a, T const b) { static_assert(std::is_integral_v<T>, "An integral type expected"); return a + b; } template <typename T, typename = void> auto compute(T const a, T const b) { static_assert(!std::is_integral_v<T>, "A non-integral type expected"); return a * b; } auto v1 = compute(1, 2); // error: ambiguous call to overloaded function auto v2 = compute(1.0, 2.0); // error: ambiguous call to overloaded function无论如何调用这个函数,最终都会出现错误,因为编译器会发现它可能调用的两个重载。这是因为 static_assert 只在重载解析完成后才被考虑。在本例中,重载解析构建了一个包含两个候选重载的集合。
这个问题的解决方案是使用 std::enable_if 和 SFINAE。我们通过模板别名 EnableIf 和 DisableIf 来使用 std::enable_if(尽管我们仍然在第二个重载上使用虚拟模板参数来引入两个不同的定义)。
下面的示例显示了重写的两个重载,第一个重载仅对整型启用,而第二个重载对整型禁用:
template <typename T, typename = EnableIf<std::is_integral<T>>> auto compute(T const a, T const b) { return a * b; } template <typename T, typename = DisableIf<std::is_integral<T>>, typename = void> auto compute(T const a, T const b) { return a + b; } auto v1 = compute(1, 2); // OK; v1 = 2 auto v2 = compute(1.0, 2.0); // OK; v2 = 3.0有了 SFINAE,当编译器为 compute(1, 2) 或 compute(1.0, 2.0) 构建重载集时,它将简单地丢弃导致替换失败的重载并继续执行,在每种情况下,我们都将最终得到包含单个候选重载的重载集。