enable_if
模板家族是一套工具,允许函数模板或类模板特化根据其模板参数的属性,将自身包含在或排除在匹配函数或特化的集合之外。例如,可以定义仅针对(因此仅匹配)特性类定义的任意类型集启用的函数模板。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(替换失败并非错误)原则 [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> {};
使用参数 B
为 true
实例化的 enable_if_c
模板包含一个成员类型 type
,定义为 T
。如果 B
为 false
,则未定义此类成员。因此,enable_if_c<B, T>::type
是有效的或无效的类型表达式,取决于 B
的值。当有效时,enable_if_c<B, T>::type
等于 T
。因此,enable_if_c
模板可用于控制何时考虑函数进行重载解析以及何时不考虑。
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 互操作,我们提供了采用任何具有名为 value
的 bool
成员常量的类型作为条件参数的 enable_if
模板版本。MPL bool_
、and_
、or_
和 not_
模板可能对创建此类类型很有用。此外,Boost.Type_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
提供了所需的行為。
编写启用程序的方式很大程度上取决于个人喜好,但对于某些函数,只可能使用一部分选项。
enable_if
必须在返回类型或额外的模板参数中使用。在支持 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
启用或禁用类模板特化。需要为启用表达式添加一个额外的模板参数。此参数的默认值为 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
模板可用于此场景,但它不使用类型特性来启用或禁用特化,而是使用 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
是一个特性类,定义了乘法运算符的结果类型。is_multipliable
特性类指定了为哪些类型启用运算符。每当 is_multipliable<A, B>::value
对于某些类型 A
和 B
为 true
时,则定义 mult_traits<A, B>::type
。
现在,尝试使用例如操作数类型 C
和 D
调用 operator*
的(其他重载),对于这些类型 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
的嵌套类型。
![]() |
注意 |
---|---|
引用特性类中的一个成员类型或静态常量会导致该特化的所有成员(类型和静态常量)都被实例化。因此,如果您的特性类有时可能包含无效类型,则应使用两个不同的模板来描述条件和类型映射。在上面的示例中, |
如果唯一区别因素是启用程序中的不同条件(即使函数永远不会不明确),某些编译器也会将函数标记为不明确。例如,某些编译器(例如 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,他们的发现影响了这个库。