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

时间:2013-01-30 09:07:28

标签: c++ templates c++11 sfinae enable-if

Scott Meyers在他的下一本书EC ++ 11上发表了content and status。 他写道,书中的一个项目可能是“在功能签名中避免std::enable_if

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

this question中显示所有三种解决方案。

作为功能参数:

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

作为模板参数:

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

作为返回类型:

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;
   }   
};
  • 应该首选哪种解决方案,为什么要避免使用其他解决方案?
  • 在哪些情况下“避免std::enable_if函数签名”关注用法作为返回类型(不是正常函数签名的一部分,而是模板特化的一部分)?
  • 会员和非会员功能模板是否存在差异?

4 个答案:

答案 0 :(得分:102)

将hack放入模板参数

模板参数方法enable_if至少比其他方法有两个优势:

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

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

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

答案 1 :(得分:55)

模板参数扣除期间,

std::enable_if依赖于“Substition Failure Is Not An Error”(又名SFINAE)原则。这是非常脆弱的语言功能,您需要非常小心才能正确使用它。

  1. 如果enable_if中的条件包含嵌套模板或类型定义(提示:查找::标记),则这些嵌套模型或类型的解析通常为非推导出的背景。在这种未推断的上下文中,任何替换失败都是错误
  2. 多个enable_if重载中的各种条件不能有任何重叠,因为重载决策是不明确的。这是作为作者需要自己检查的内容,尽管您会收到良好的编译器警告。
  3. enable_if在重载决策期间操纵可行功能集,这可能具有令人惊讶的交互,这取决于从其他范围引入的其他功能的存在(例如通过ADL)。这使它不是很强大。
  4. 简而言之,当它工作时它起作用,但是当它不起作用时,它可能非常难以调试。一个非常好的选择是使用标签调度,即委托给一个实现函数(通常在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 */) {}
    }
    

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

答案 2 :(得分:4)

  

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

  • 模板参数

    • 可在构造函数中使用。
    • 可在用户定义的转换运算符中使用。
    • 它需要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()
      

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

  • 返回类型:

    • 它不能在构造函数中使用。 (无回报类型)
    • 不能在用户定义的转换运算符中使用。 (不可扣除)
    • 可以使用pre-C ++ 11。
    • 第二个更易读的IMO。
  • 最后,在函数参数中:

    • 可以使用pre-C ++ 11。
    • 可在构造函数中使用。
    • 不能在用户定义的转换运算符中使用。 (无参数)
    • 它不能用于具有固定数量参数的方法(一元/二元运算符+-*,...)
    • 它可以安全地用于继承(见下文)。
    • 更改函数签名(基本上有一个额外的最后一个参数void* = nullptr)(因此函数指针会有所不同,等等)
  

会员和非会员功能模板是否有任何差异?

与继承和using

存在细微差别

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

namespace.udecl

  

using-declarator引入的声明集是通过对using-declarator中的名称执行限定名查找([basic.lookup.qual],[class.member.lookup])来找到的,不包括函数隐藏如下所述。

     

...

     

当using-declarator将基类的声明带入派生类时,派生类中的成员函数和成员函数模板覆盖和/或隐藏成员函数和成员函数模板,其名称相同,参数 - 基类中的type-list,cv-qualification和ref-qualifier(如果有的话)(而不是冲突)。这些隐藏或重写的声明被排除在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() {}
};

Demo(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) {}
};

Demo

答案 3 :(得分:0)

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

提出问题时,std::enable_if 中的 <type_traits> 是可用的最佳工具,其他答案在 C++17 之前都是合理的。

现在在 C++20 中,我们通过 requires 直接支持编译器。

#include <concepts
template<typename T>
struct Check20
{
   template<typename U = T>
   U read() requires std::same_as <U, int>
   { return 42; }
   
   template<typename U = T>
   U read() requires std::same_as <U, double>
   { return 3.14; }   
};