enable_if
模板家族是一组工具,用于根据模板参数的属性,允许函数模板或类模板特化将其自身包含或排除在一组匹配的函数或特化中。例如,可以定义仅对由 traits 类定义的任意一组类型启用的、因此只匹配这些类型的函数模板。enable_if
模板也可以应用于启用类模板特化。enable_if
的应用在 [1] 和 [2] 中有详细讨论。
namespace boost { template <class Cond, class T = void> struct enable_if; template <class Cond, class T = void> struct disable_if; template <class Cond, class T> struct lazy_enable_if; template <class Cond, class T> struct lazy_disable_if; template <bool B, class T = void> struct enable_if_c; template <bool B, class T = void> struct disable_if_c; template <bool B, class T> struct lazy_enable_if_c; template <bool B, class T> struct lazy_disable_if_c; }
C++ 中模板函数重载的合理运行依赖于 SFINAE (substitution-failure-is-not-an-error,替换失败不是错误) 原理 [3]:如果在函数模板的实例化过程中形成了一个无效的参数类型或返回类型,该实例化将从重载解析集中移除,而不是导致编译错误。以下示例摘自 [1],展示了这一点的重要性。
int negate(int i) { return -i; } template <class F> typename F::result_type negate(const F& f) { return -f(); }
假设编译器遇到调用 negate(1)
。第一个定义显然是更好的匹配,但编译器仍必须考虑(并实例化其原型)这两个定义才能得出此结论。使用 F
作为 int
来实例化后一个定义将导致
int::result_type negate(const int&);
其中返回类型是无效的。如果这是一个错误,添加一个不相关的函数模板(从未被调用过)可能会破坏其他有效的代码。然而,由于 SFINAE 原理,上述示例并非错误。后一个 `negate` 定义只是从重载解析集中移除。
enable_if
模板是用于受控创建 SFINAE 条件的工具。
enable_if
模板的名称有三个部分:一个可选的 lazy_
标签,可以是 enable_if
或 disable_if
,以及一个可选的 _c
标签。所有这三部分的八种组合都受支持。lazy_
标签的含义在 下方 的部分中进行了描述。名称的第二部分指示一个真条件参数应该启用还是禁用当前重载。名称的第三部分指示条件参数是 bool
值(带有 _c
后缀),还是包含一个名为 value
的静态 bool
常量的类型(没有后缀)。后一种版本与 Boost.MPL 互操作。
enable_if_c
和 enable_if
的定义如下(我们使用非限定名称的 enable_if
模板,但它们在 boost
命名空间中)。
template <bool B, class T = void> struct enable_if_c { typedef T type; }; template <class T> struct enable_if_c<false, T> {}; template <class Cond, class T = void> struct enable_if : public enable_if_c<Cond::value, T> {};
当 enable_if_c
模板的参数 B
为 true
时,其实例化包含一个名为 type
的成员类型,定义为 T
。如果 B
为 false
,则不定义该成员。因此,enable_if_c<B, T>::type
根据 B
的值,要么是有效的类型表达式,要么是无效的类型表达式。当有效时,enable_if_c<B, T>::type
等于 T
。因此,enable_if_c
模板可用于控制函数何时被考虑用于重载解析,何时不被考虑。例如,以下函数对所有算术类型(根据 Boost type_traits 库的分类)都进行了定义。
template <class T> typename enable_if_c<boost::is_arithmetic<T>::value, T>::type foo(T t) { return t; }
disable_if_c
模板也提供,并且除了条件取反外,功能与 enable_if_c
相同。以下函数对所有非算术类型启用。
template <class T> typename disable_if_c<boost::is_arithmetic<T>::value, T>::type bar(T t) { return t; }
为了在某些情况下更方便语法以及与 Boost.MPL 互操作,我们提供了 enable_if
模板的版本,它们接受任何带有名为 value
的 bool
成员常量的类型作为条件参数。MPL 的 bool_
、and_
、or_
和 not_
模板很可能有助于创建此类类型。此外,Boost.Type_traits 库中的 traits 类也遵循此约定。例如,上面示例中的函数 foo
也可以这样写:
template <class T> typename enable_if<boost::is_arithmetic<T>, T>::type foo(T t) { return t; }
enable_if
模板定义在 boost/utility/enable_if.hpp
中,该文件被 boost/utility.hpp
包含。
就函数模板而言,enable_if
可以通过多种不同的方式使用:
在前一节中,展示了 `enable_if` 的返回类型形式。作为使用通过额外函数参数工作的 `enable_if` 形式的示例,上一节中的 `foo` 函数也可以这样编写:
template <class T> T foo(T t, typename enable_if<boost::is_arithmetic<T> >::type* dummy = 0);
因此,添加了一个类型为 void*
的额外参数,但它被赋予了默认值,以将该参数隐藏起来不被客户端代码调用。请注意,第二个模板参数未传递给 `enable_if`,因为默认值 void
提供了所需的行为。
选择哪种方式来编写 enabler 在很大程度上是个人品味问题,但对于某些函数,只有部分选项是可行的:
在支持 C++0x 函数模板参数默认参数的编译器中,您可以通过添加一个额外的模板参数来启用和禁用函数模板。这种方法适用于您将使用 `enable_if` 的返回类型形式或函数参数形式的所有情况,包括运算符、构造函数、可变函数模板,甚至重载的转换操作。
举个例子
#include <boost/type_traits/is_arithmetic.hpp> #include <boost/type_traits/is_pointer.hpp> #include <boost/utility/enable_if.hpp> class test { public: // A constructor that works for any argument list of size 10 template< class... T, typename boost::enable_if_c< sizeof...( T ) == 10, int >::type = 0> test( T&&... ); // A conversion operation that can convert to any arithmetic type template< class T, typename boost::enable_if< boost::is_arithmetic< T >, int >::type = 0> operator T() const; // A conversion operation that can convert to any pointer type template< class T, typename boost::enable_if< boost::is_pointer< T >, int >::type = 0> operator T() const; }; int main() { // Works test test_( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ); // Fails as expected test fail_construction( 1, 2, 3, 4, 5 ); // Works by calling the conversion operator enabled for arithmetic types int arithmetic_object = test_; // Works by calling the conversion operator enabled for pointer types int* pointer_object = test_; // Fails as expected struct {} fail_conversion = test_; }
类模板特化可以使用 `enable_if` 进行启用或禁用。需要添加一个额外的模板参数来用于 enabler 表达式。此参数的默认值为 `void`。例如:
template <class T, class Enable = void> class A { ... }; template <class T> class A<T, typename enable_if<is_integral<T> >::type> { ... }; template <class T> class A<T, typename enable_if<is_float<T> >::type> { ... };
使用任何整数类型实例化 `A` 将匹配第一个特化,而任何浮点类型将匹配第二个特化。所有其他类型都匹配主模板。条件可以是任何依赖于类模板参数的编译时布尔表达式。请注意,再次不需要为 `enable_if` 提供第二个参数;默认值 (`void`) 是正确的值。
enable_if_has_type
模板在这种情况下可用,但它不使用类型 trait 来启用或禁用特化,而是使用 SFINAE 上下文来检查其参数中是否存在依赖类型。例如,以下结构体仅当 `T::value_type` 存在时,才从 T 中提取一个依赖的 `value_type`。
template <class T, class Enable = void> class value_type_from { typedef T type; }; template <class T> class value_type_from<T, typename enable_if_has_type<typename T::value_type>::type> { typedef typename T::value_type type; };
一旦编译器检查了启用条件并将函数包含在重载解析集中,就会使用常规的 C++ 重载解析规则来选择最佳匹配的函数。特别是,启用条件之间没有排序。具有非互斥启用条件的函数模板可能导致歧义。例如:
template <class T> typename enable_if<boost::is_integral<T>, void>::type foo(T t) {} template <class T> typename enable_if<boost::is_arithmetic<T>, void>::type foo(T t) {}
所有整数类型也是算术类型。因此,例如,对于调用 foo(1)
,两个条件都为真,因此这两个函数都包含在重载解析集中。它们都是同样好的匹配,因此是模棱两可的。当然,只要其他参数消除了函数的歧义,就可以同时为多个启用条件为真。
上述讨论也适用于在类模板部分特化中使用 `enable_if`。
在某些情况下,有必要在启用条件为真之前避免实例化函数签名的一部分。例如:
template <class T, class U> class mult_traits; template <class T, class U> typename enable_if<is_multipliable<T, U>, typename mult_traits<T, U>::type>::type operator*(const T& t, const U& u) { ... }
假设类模板 mult_traits
是一个 traits 类,定义了乘法运算符的结果类型。is_multipliable
traits 类指定了哪个类型可以启用该运算符。每当对于某些类型 A
和 B
,is_multipliable<A, B>::value
为 true
时,mult_traits<A, B>::type
才会被定义。
现在,尝试使用(某个其他重载的)`operator*`,例如,其操作数类型为 C
和 D
,对于这些类型 is_multipliable<C, D>::value
为 false
且 mult_traits<C, D>::type
未定义,这在某些编译器上会出错。SFINAE 原理未被应用,因为无效类型出现在另一个模板的参数中。lazy_enable_if
和 lazy_disable_if
模板(及其 _c
版本)可用于此类情况。
template<class T, class U> typename lazy_enable_if<is_multipliable<T, U>, mult_traits<T, U> >::type operator*(const T& t, const U& u) { ... }
lazy_enable_if
的第二个参数必须是一个类类型,当第一个参数(条件)为真时,该类类型定义一个名为 type
的嵌套类型。
![]() |
注意 |
---|---|
引用 traits 类中的一个成员类型或静态常量会导致该特化中的所有成员(类型和静态常量)被实例化。因此,如果您的 traits 类有时可能包含无效类型,您应该使用两个不同的模板来描述条件和类型映射。在上面的示例中, |
一些编译器会将函数标记为模棱两可,如果唯一的区别在于 enabler 中的不同条件(即使函数永远不应模棱两可)。例如,某些编译器(如 GCC 3.2)会诊断以下两个函数为模棱两可:
template <class T> typename enable_if<boost::is_arithmetic<T>, T>::type foo(T t); template <class T> typename disable_if<boost::is_arithmetic<T>, T>::type foo(T t);
可以应用两种变通方法:
使用一个额外的虚拟参数来消除函数的歧义。为其使用默认值以将参数隐藏起来不被调用者看到。例如:
template <int> struct dummy { dummy(int) {} }; template <class T> typename enable_if<boost::is_arithmetic<T>, T>::type foo(T t, dummy<0> = 0); template <class T> typename disable_if<boost::is_arithmetic<T>, T>::type foo(T t, dummy<1> = 0);
在不同的命名空间中定义函数,并使用 `using` 声明将它们引入一个公共命名空间:
namespace A { template <class T> typename enable_if<boost::is_arithmetic<T>, T>::type foo(T t); } namespace B { template <class T> typename disable_if<boost::is_arithmetic<T>, T>::type foo(T t); } using A::foo; using B::foo;
请注意,上面的第二个变通方法不能用于成员模板。另一方面,运算符不接受额外参数,这使得第一个变通方法无法使用。因此,对于需要定义为成员函数的模板化运算符(赋值和下标运算符),这两种变通方法都无济于事。
我们感谢 Howard Hinnant、Jason Shirk、Paul Mensonides 和 Richard Smith 的发现对本库的影响。