为什么我应该避免函数签名中的std :: enable_if

han*_*aad 163 c++ templates sfinae enable-if c++11

Scott Meyers发表了他的下一本书EC++ 11的内容和状态.他写道,书中的一个项目可能是"避免std::enable_if功能签名".

std::enable_if 可以用作函数参数,返回类型或类模板或函数模板参数,以有条件地从重载解析中删除函数或类.

这个问题中,显示了所有三个解决方案

作为功​​能参数:

template<typename T>
struct Check1
{
   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, int>::value >::type* = 0) { return 42; }

   template<typename U = T>
   U read(typename std::enable_if<
          std::is_same<U, double>::value >::type* = 0) { return 3.14; }   
};
Run Code Online (Sandbox Code Playgroud)

作为模板参数:

template<typename T>
struct Check2
{
   template<typename U = T, typename std::enable_if<
            std::is_same<U, int>::value, int>::type = 0>
   U read() { return 42; }

   template<typename U = T, typename std::enable_if<
            std::is_same<U, double>::value, int>::type = 0>
   U read() { return 3.14; }   
};
Run Code Online (Sandbox Code Playgroud)

作为返回类型:

template<typename T>
struct Check3
{
   template<typename U = T>
   typename std::enable_if<std::is_same<U, int>::value, U>::type read() {
      return 42;
   }

   template<typename U = T>
   typename std::enable_if<std::is_same<U, double>::value, U>::type read() {
      return 3.14;
   }   
};
Run Code Online (Sandbox Code Playgroud)
  • 应该首选哪种解决方案,为什么要避开其他解决方案?
  • 在哪些情况下,"避免std::enable_if在函数签名中"将使用视为返回类型(不是正常函数签名的一部分,而是模板特化的一部分)?
  • 成员和非成员函数模板是否有任何差异?

R. *_*des 106

将hack放入模板参数中.

enable_if模板参数方法比别人至少有两个好处:

  • 可读性:enable_if use和return/argument类型不会合并为一个混乱的typename disambiguators和嵌套类型访问块; 即使可以使用别名模板来缓解消歧器和嵌套类型的混乱,但仍然会将两个不相关的事物合并在一起.enable_if use与模板参数有关,而与返回类型无关.将它们放在模板参数中意味着它们更接近重要的事物;

  • 普遍适用性:构造函数没有返回类型,并且一些运算符不能有额外的参数,因此其他两个选项都不能应用于任何地方.将enable_if放在模板参数中无处不在,因为您只能在模板上使用SFINAE.

对我来说,可读性方面是这种选择的重要推动因素.

  • 您还可以使用别名模板和一些调整使其更具可读性:http://flamingdangerzone.com/cxx11/2012/06/01/almost-static-if.html (4认同)
  • 使用`FUNCTION_REQUIRES`宏[这里](http://stackoverflow.com/a/9220563/375343),使它更好阅读,它也适用于C++ 03编译器,它依赖于使用`返回类型中的enable_if`.此外,在函数模板参数中使用`enable_if`会导致重载问题,因为现在函数签名不是唯一的,从而导致模糊的重载错误. (4认同)
  • 这是一个老问题,但对于仍在阅读的人:@Paul提出的问题的解决方案是使用带有默认非类型模板参数的`enable_if`,这允许重载.即`enable_if_t <condition,int> = 0`而不是`typename = enable_if_t <condition>`. (2认同)

Tem*_*Rex 56

std::enable_if模板参数推导期间依赖于" 替换失败不是错误 "(又名SFINAE)原则.这是一个非常脆弱的语言功能,你需要非常小心地做到正确.

  1. 如果你的条件enable_if包含嵌套模板或类型定义(提示:查找::标记),那么这些嵌套的模型或类型的解析通常是非推导的上下文.在这种未推断的上下文上的任何替换失败都是错误的.
  2. 多次enable_if重载中的各种条件不能有任何重叠,因为重载分辨率会模糊不清.这是作为作者需要自己检查的内容,尽管您会收到良好的编译器警告.
  3. enable_if在重载决策期间操纵可行功能集,这可能具有令人惊讶的交互,这取决于从其他范围引入的其他功能的存在(例如通过ADL).这使它不是很强大.

简而言之,当它工作时,它可以工作,但是当它不工作时,它可能很难调试.一个非常好的替代方法是使用标签分派,即委托给一个实现函数(通常在detail命名空间或帮助器类中),它接收基于你在中使用的相同编译时条件的伪参数enable_if.

template<typename T>
T fun(T arg) 
{ 
    return detail::fun(arg, typename some_template_trait<T>::type() ); 
}

namespace detail {
    template<typename T>
    fun(T arg, std::false_type /* dummy */) { }

    template<typename T>
    fun(T arg, std::true_type /* dummy */) {}
}
Run Code Online (Sandbox Code Playgroud)

标签分派不会操纵重载集,但可以通过编译时表达式(例如,在类型特征中)提供适当的参数来帮助您精确选择所需的函数.根据我的经验,这更容易调试和正确.如果你是一个有着复杂类型特征的有抱负的图书馆作家,你可能需要enable_if某种方式,但对于大多数经常使用的编译时条件,不建议这样做.

  • 标签调度有一个缺点:如果你有一些检测到函数存在的特性,并且该函数是使用标签调度方法实现的,它总是将该成员报告为存在,并导致错误而不是潜在的替换失败.SFINAE主要是一种从候选集中删除重载的技术,而标签调度是一种在两个(或更多)重载之间进行选择的技术.功能上有一些重叠,但它们并不相同. (22认同)
  • SFINAE"脆弱"?什么? (8认同)

Jar*_*d42 5

应该首选哪种解决方案,为什么我应该避免其他解决方案?

  • 模板参数

    • 在构造函数中可用。
    • 在用户定义的转换运算符中可用。
    • 它需要C ++ 11或更高版本。
    • 它是IMO,更具可读性。
    • 它可能很容易被错误地使用,并产生带有过载的错误:

      template<typename T, typename = std::enable_if_t<std::is_same<T, int>::value>>
      void f() {/*...*/}
      
      template<typename T, typename = std::enable_if_t<std::is_same<T, float>::value>>
      void f() {/*...*/} // Redefinition: both are just template<typename, typename> f()
      
      Run Code Online (Sandbox Code Playgroud)

    注意typename = std::enable_if_t<cond>而不是正确std::enable_if_t<cond, int>::type = 0

  • 返回类型:

    • 不能在构造函数中使用。(无返回类型)
    • 它不能在用户定义的转换运算符中使用。(不可推论)
    • 可以使用C ++ 11之前的版本。
    • 其次是更具可读性的IMO。
  • 最后,在函数参数中:

    • 可以使用C ++ 11之前的版本。
    • 在构造函数中可用。
    • 它不能在用户定义的转换运算符中使用。(无参数)
    • 它不能与固定数目的参数的方法中使用(一元/二进制运算符+-*,...)
    • 可以安全地在继承中使用(请参见下文)。
    • 更改函数签名(基本上,最后一个参数是多余的void* = nullptr)(因此函数指针会有所不同,依此类推)

成员函数模板和非成员函数模板有什么区别吗?

继承和之间有细微的差异using

根据using-declarator(强调我的):

namespace.udecl

通过对using-declarator中的名称执行合格的名称查找([basic.lookup.qual],[class.member.lookup]),可以找到using-declarator引入的声明集,但不包括按说明隐藏的函数下面。

...

当using-declarator将基类的声明带入派生类时,派生类中的成员函数和成员函数模板将覆盖和/或隐藏具有相同名称,parameter-type-list,cv-的成员函数和成员函数模板。限定和ref限定符(如果有)在基类中(而不是冲突)。此类隐藏或覆盖的声明从using-declarator引入的声明集中排除。

因此,对于模板参数和返回类型,在以下情况下方法都是隐藏的:

struct Base
{
    template <std::size_t I, std::enable_if_t<I == 0>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 0> g() {}
};

struct S : Base
{
    using Base::f; // Useless, f<0> is still hidden
    using Base::g; // Useless, g<0> is still hidden

    template <std::size_t I, std::enable_if_t<I == 1>* = nullptr>
    void f() {}

    template <std::size_t I>
    std::enable_if_t<I == 1> g() {}
};
Run Code Online (Sandbox Code Playgroud)

演示(gcc错误地找到基本函数)。

鉴于有论点,类似的情况适用:

struct Base
{
    template <std::size_t I>
    void h(std::enable_if_t<I == 0>* = nullptr) {}
};

struct S : Base
{
    using Base::h; // Base::h<0> is visible

    template <std::size_t I>
    void h(std::enable_if_t<I == 1>* = nullptr) {}
};
Run Code Online (Sandbox Code Playgroud)

演示版