Boost C++ 库

……这是世界上最受推崇、设计最精妙的 C++ 库项目之一。 Herb SutterAndrei AlexandrescuC++ Coding Standards

enable_if - Boost C++ 函数库
PrevUpHomeNext

作者

  • Jaakko Järvi
  • Jeremiah Willcock
  • Andrew Lumsdaine

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_ifdisable_if,以及一个可选的 _c 标签。所有这三部分的八种组合都受支持。lazy_ 标签的含义在 下方 的部分中进行了描述。名称的第二部分指示一个真条件参数应该启用还是禁用当前重载。名称的第三部分指示条件参数是 bool 值(带有 _c 后缀),还是包含一个名为 value 的静态 bool 常量的类型(没有后缀)。后一种版本与 Boost.MPL 互操作。

enable_if_cenable_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 模板的参数 Btrue 时,其实例化包含一个名为 type 的成员类型,定义为 T。如果 Bfalse,则不定义该成员。因此,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 模板的版本,它们接受任何带有名为 valuebool 成员常量的类型作为条件参数。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 可以通过多种不同的方式使用:

  • 作为实例化函数的返回类型
  • 作为实例化函数的额外参数
  • 作为额外的模板参数(仅在支持 C++0x 函数模板参数默认参数的编译器中很有用,有关详细信息,请参阅 C++0x 中的函数模板启用)。

在前一节中,展示了 `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 在很大程度上是个人品味问题,但对于某些函数,只有部分选项是可行的:

  • 许多运算符具有固定数量的参数,因此 `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` 进行启用或禁用。需要添加一个额外的模板参数来用于 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 类指定了哪个类型可以启用该运算符。每当对于某些类型 ABis_multipliable<A, B>::valuetrue 时,mult_traits<A, B>::type 才会被定义。

现在,尝试使用(某个其他重载的)`operator*`,例如,其操作数类型为 CD,对于这些类型 is_multipliable<C, D>::valuefalsemult_traits<C, D>::type 未定义,这在某些编译器上会出错。SFINAE 原理未被应用,因为无效类型出现在另一个模板的参数中。lazy_enable_iflazy_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 的嵌套类型。

[Note] 注意

引用 traits 类中的一个成员类型或静态常量会导致该特化中的所有成员(类型和静态常量)被实例化。因此,如果您的 traits 类有时可能包含无效类型,您应该使用两个不同的模板来描述条件和类型映射。在上面的示例中,is_multipliable<T, U>::value 定义了 mult_traits<T, U>::type 何时有效。

一些编译器会将函数标记为模棱两可,如果唯一的区别在于 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 的发现对本库的影响。

  • [1] Jaakko Järvi, Jeremiah Willcock, Howard Hinnant, and Andrew Lumsdaine. Function overloading based on arbitrary properties of types. C++ Users Journal, 21(6):25--32, June 2003.
  • [2] Jaakko Järvi, Jeremiah Willcock, and Andrew Lumsdaine. Concept-controlled polymorphism. In Frank Pfennig and Yannis Smaragdakis, editors, Generative Programming and Component Engineering, volume 2830 of LNCS, pages 228--244. Springer Verlag, September 2003.
  • [3] David Vandevoorde and Nicolai M. Josuttis. C++ Templates: The Complete Guide. Addison-Wesley, 2002.

PrevUpHomeNext