当前位置: 首页 > 知识库问答 >
问题:

为什么要避免函数签名中的std::enable\u

袁恩
2023-03-14

Scott Meyers发布了他的下一本书EC 11的内容和状态。他写道,书中的一项可以是“在函数签名中避免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; }   
};

作为模板参数:

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;
   }   
};
  • 哪种解决方案应该是首选的,为什么我要避开其他人?
  • 在哪些情况下,函数签名中避免d::estnable_if涉及作为返回类型的使用(这不是普通函数签名的一部分,而是模板特化的一部分)?
  • 成员和非成员函数模板有什么区别吗?

共有3个答案

夏侯元忠
2023-03-14

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

>

它可用于用户定义的转换运算符。

它需要C 11或更高版本。

在我看来,它更具可读性(C 20之前)。

重载很容易被误用并产生错误:

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\u if\u t

  • 它不能与构造函数(没有返回类型)一起使用
  • 它不能在用户定义的转换运算符中使用(因为它是不可推导的)
  • 它可以在C 11之前使用。
  • 第二个更具可读性的IMO(预C 20)。
  • 可以使用pre-C 11
  • 它在构造函数中可用
  • 它不能用于用户定义的转换运算符(它们没有参数)
  • 它不能用于具有固定数量参数的方法中,例如一元/二元运算符、一元/二元运算符、一元/二元运算符、一元/二元运算符、一元/二元运算符、一元/二元/二元运算符、一元/二元/二元运算符、一元/二元/二元运算符、一元/二元/二元运算符、一元/二元运算符、二元/二元
  • 在继承中使用它是安全的(见下文)
  • 更改函数签名(您基本上有一个额外的as-last参数void*=nullptr);这会导致指向函数的指针行为不同等等

现在有要求子句

>

  • 它在构造函数中可用

    它可用于用户定义的转换运算符。

    它需要C 20

    IMO,最具可读性

    与继承一起使用是安全的(见下文)。

    可以直接使用类的模板参数

    template <typename T>
    struct Check4
    {
       T read() requires(std::is_same<T, int>::value) { return 42; }
       T read() requires(std::is_same<T, double>::value) { return 3.14; }   
    };
    

    成员和非成员函数模板有什么不同吗?

    继承和使用有细微的区别:

    根据使用声明符(emphasis mine)的

    namespace.udecl

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

    ...

    当使用声明符将基类中的声明引入派生类时,派生类中的成员函数和成员函数模板会覆盖和/或隐藏基类中具有相同名称、参数类型列表、cv限定和ref限定符(如果有)的成员函数和成员函数模板(而不是冲突)。此类隐藏或覆盖的声明被排除在使用声明符引入的声明集中。

    因此,对于模板参数和返回类型,隐藏方法的场景如下:

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

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

    演示

    使用也需要:

    struct Base
    {
        template <std::size_t I>
        void f() requires(I == 0) { std::cout << "Base f 0\n";}
    };
    
    struct S : Base
    {
        using Base::f;
        
        template <std::size_t I>
        void f() requires(I == 1) {}
    };
    

    演示

  • 贺功
    2023-03-14

    如果在模板参数推导过程中依赖“子项失败不是错误”(也称为SFINAE)原则,则启用。这是一个非常脆弱的语言特性,您需要非常小心才能正确使用它。

    1. 如果您在enable_if中的条件包含嵌套模板或类型定义(提示:查找::标记),那么这些嵌套模板或类型的解析通常是一个非推导上下文。在这样的非推导上下文上的任何替换失败都是错误的。
    2. 多个enable_if重载中的各种条件不能有任何重叠,因为重载解析将是模棱两可的。作为作者,您需要检查自己,尽管您会收到很好的编译器警告。
    3. enable_if在重载解析期间操作一组可行的函数,这可能会有令人惊讶的交互,具体取决于从其他范围(例如通过ADL)引入的其他函数的存在。这使得它不是很健壮。

    简而言之,当它工作时,它工作,但当它不工作时,调试可能非常困难。一个很好的替代方法是使用标记调度,即委托给实现函数(通常在详细名称空间或帮助器类中),该函数基于您在enable\U 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 */) {}
    }
    

    标记调度不会操作重载集,但通过编译时表达式(例如在类型特征中)提供适当的参数,可以帮助您准确地选择所需的函数。根据我的经验,这更容易调试和正确。如果您是一位有抱负的具有复杂类型特征的库编写者,您可能需要在某种程度上启用If,但对于大多数编译时条件的常规使用,不建议这样做。

    樊烨烨
    2023-03-14

    在模板参数中加入黑客攻击。

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

    >

  • 易读性:enable_if使用和返回/参数类型没有合并到一个混乱的类型名消歧器和嵌套类型访问块中;即使消歧器和嵌套类型的混乱可以通过别名模板来缓解,但这仍然会将两个不相关的东西合并在一起。enable_if使用与模板参数无关,与返回类型无关。模板参数中包含它们意味着它们更接近重要的东西;

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

    对我来说,可读性是这一选择的主要动机。

  •  类似资料:
    • 问题内容: 通常我会尽可能避免转换类型,因为我认为这是不良的编码实践,并且可能会导致性能下降。 但是,如果有人要我解释为什么会这样,我可能会像前灯中的鹿一样看它们。 那么,为什么/何时铸造不好? 它对于Java,C#,C ++是通用的,还是每个不同的运行时环境都按照自己的方式处理? 欢迎使用任何语言的细节,例如为什么在c ++中不好? 问题答案: 您已经用三种语言标记了这三种语言,答案在三种语言之

    • 在React中,我尝试了两种方法: 然后更改状态this.setState(this.state) 克隆状态,更改状态克隆,然后更改此.setState(stateClone) 它们都起作用,产生相同的结果。为什么建议(在文档中)设置为状态克隆(使用Object.assign),而不是设置为状态本身?状态的对象标识在React中重要吗(没有Redux)?似乎只要调用setState,不管状态对象标

    • 我正在创建一个带有片段的应用程序,在其中一个片段中,我创建了一个非默认构造函数,并收到以下警告: 有人能告诉我为什么这不是个好主意吗? 你能否建议我如何做到这一点: 不使用非默认构造函数?

    • 问题内容: 我已经在多个地方多次看到过这种情况,但是从未找到令人满意的解释来说明为什么会这样。 因此,希望这里会介绍一个。为什么我们(至少通常)不使用和? 编辑:我看到人们以为这个问题与Web服务器有关,但事实并非如此。我可以理解为什么传递给未经处理的字符串可能很糟糕。在非Web应用程序中不好吗? 问题答案: 通常有更清晰,更直接的方法来获得相同的效果。如果构建复杂的字符串并将其传递给,则代码将难

    • 问题内容: Process p = Runtime.getRuntime().exec(command); is = p.getInputStream(); byte[] userbytes = new byte[1024]; is.read(userbytes); 我想从java在linux os中执行shell命令。但是Pmd报告说不要使用Java Runtime.exec()。为什么?是什么

    • 在下面的代码中,为什么允许绑定到函数? 演示 为什么允许这样做的直觉是什么,如何实现它,以及当您向传递一个lvalue参数,然后它调用接受rvalue引用的函数时,它实际上意味着什么?