Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb SutterAndrei Alexandrescu, C++ 编码标准

PrevUpHomeNext

enable_if

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

作者

  • 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(替换失败不是错误)原则 [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_ 标签的含义在 下面的 Lazy enable_if 章节中描述。名称的第二部分指示真条件参数应启用还是禁用当前重载。名称的第三部分指示条件参数是 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 等于 Tenable_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 给出了期望的行为。

以哪种方式编写启用器在很大程度上是个人喜好问题,但对于某些函数,只有一部分选项是可能的

  • 许多运算符具有固定数量的参数,因此 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 模板在此场景中可用,但它不是使用类型 traits 来启用或禁用特化,而是使用 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 类指定为哪些类型启用运算符。每当 is_multipliable<A, B>::value 对于某些类型 ABtrue 时,则定义 mult_traits<A, B>::type

现在,尝试使用操作数类型 CD 调用 operator* 的(某些其他重载),对于这些类型,is_multipliable<C, D>::valuefalse 并且未定义 mult_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 有效。

如果唯一的区分因素是启用器中的不同条件(即使函数永远不可能是模棱两可的),则某些编译器会将函数标记为模棱两可。例如,某些编译器(例如 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++ Users Journal, 21(6):25--32, 2003 年 6 月。
  • [2] Jaakko Järvi, Jeremiah Willcock, 和 Andrew Lumsdaine. 概念控制的多态性。在 Frank Pfennig 和 Yannis Smaragdakis, 编辑, Generative Programming and Component Engineering, 卷 2830 的 LNCS, 页码 228--244. Springer Verlag, 2003 年 9 月。
  • [3] David Vandevoorde 和 Nicolai M. Josuttis. C++ 模板:完整指南. Addison-Wesley, 2002.

PrevUpHomeNext