Boost C++ 库

……世界上最受推崇和设计精良的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu,《C++ 编码规范

PrevUpHomeNext

enable_if

简介
enable_if 模板
使用 enable_if
致谢
参考文献

作者

  • Jaakko Järvi
  • Jeremiah Willcock
  • Andrew Lumsdaine

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_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> {};

使用参数 Btrue 实例化的 enable_if_c 模板包含一个成员类型 type,定义为 T。如果 Bfalse,则未定义此类成员。因此,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 互操作,我们提供了采用任何具有名为 valuebool 成员常量的类型作为条件参数的 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 可以以多种不同的方式使用。

  • 作为实例化函数的返回类型
  • 作为实例化函数的额外参数
  • 作为额外的模板参数(仅在支持 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 提供了所需的行為。

编写启用程序的方式很大程度上取决于个人喜好,但对于某些函数,只可能使用一部分选项。

  • 许多运算符具有固定数量的参数,因此 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 对于某些类型 ABtrue 时,则定义 mult_traits<A, B>::type

现在,尝试使用例如操作数类型 CD 调用 operator* 的(其他重载),对于这些类型 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] 注意

引用特性类中的一个成员类型或静态常量会导致该特化的所有成员(类型和静态常量)都被实例化。因此,如果您的特性类有时可能包含无效类型,则应使用两个不同的模板来描述条件和类型映射。在上面的示例中,is_multipliable<T, U>::value 定义了 mult_traits<T, U>::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,他们的发现影响了这个库。

  • [1] Jaakko Järvi、Jeremiah Willcock、Howard Hinnant 和 Andrew Lumsdaine。基于类型的任意属性的函数重载。C++ 用户期刊,21(6):25--32,2003 年 6 月。
  • [2] Jaakko Järvi、Jeremiah Willcock 和 Andrew Lumsdaine。概念控制的多态性。在 Frank Pfennig 和 Yannis Smaragdakis 编辑的 生成式编程和组件工程 中,LNCS 的 2830 卷,第 228--244 页。施普林格出版社,2003 年 9 月。
  • [3] David Vandevoorde 和 Nicolai M. Josuttis。C++ 模板:完整指南。Addison-Wesley,2002 年。

PrevUpHomeNext