【问题标题】:Why should I avoid std::enable_if in function signatures为什么我应该在函数签名中避免 std::enable_if
【发布时间】:2013-01-14 01:03:53
【问题描述】:

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 涉及用作返回类型(它不是普通函数签名的一部分,而是模板特化的一部分)?
  • 成员函数模板和非成员函数模板有什么区别吗?

【问题讨论】:

  • 因为重载通常也一样好。如果有的话,委托给使用(专门的)类模板的实现。
  • 好吧,只是主观上我不得不说,虽然经常非常有用,但我不喜欢 std::enable_if 弄乱我的函数签名(尤其是丑陋的附加 nullptr 函数参数版本),因为它总是看起来它是什么,一个奇怪的黑客(static if 可能会做得更漂亮和干净)使用模板黑魔法来利用有趣的语言功能。这就是为什么我尽可能喜欢标签调度(嗯,你仍然有额外的奇怪参数,但不是在公共接口中,而且更不用说丑陋和神秘)。
  • 我想问一下=0中的typename std::enable_if&lt;std::is_same&lt;U, int&gt;::value, int&gt;::type = 0是做什么的?我找不到正确的资源来理解它。我知道=0 之前的第一部分有一个成员类型int 如果Uint 相同。非常感谢!
  • @astroboyrx 有趣的是,我只是想发表评论指出这一点。基本上,=0 表示这是一个默认的非类型模板参数。这样做是因为默认的类型模板参数不是签名的一部分,所以你不能重载它们。
  • 赞成这个问题,因为它有所有使用 enable_if 的方法! (;

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


【解决方案1】:

将 hack 放入模板参数中

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

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

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

对我来说,可读性方面是这个选择的最大动力。

【讨论】:

  • 使用 FUNCTION_REQUIREShere 使其更易于阅读,并且它也适用于 C++03 编译器,并且它依赖于在返回类型中使用 enable_if。此外,在函数模板参数中使用enable_if 会导致重载问题,因为现在函数签名不是唯一的,会导致模棱两可的重载错误。
  • 这是一个老问题,但对于仍在阅读的人来说:@Paul 提出的问题的解决方案是将enable_if 与默认的非类型模板参数一起使用,该参数允许重载。 IE。 enable_if_t&lt;condition, int&gt; = 0 而不是 typename = enable_if_t&lt;condition&gt;
  • @R.MartinhoFernandes 您评论中的flamingdangerzone 链接现在似乎指向一个间谍软件安装页面。我标记了它以引起版主的注意。
【解决方案2】:

std::enable_if模板参数推导期间依赖于“Substition Failure Is Not An Error”(又名 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 */) {}
}

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

【讨论】:

  • 标签调度有一个缺点:如果你有一些特征可以检测到一个函数的存在,并且这个函数是用标签调度方法实现的,它总是报告那个成员存在,并导致一个错误而不是潜在的替换失败。 SFINAE 主要是一种从候选集中去除重载的技术,而标签调度是一种在两个(或多个)重载之间进行选择的技术。功能上有一些重叠,但它们并不等同。
  • @R.MartinhoFernandes 你能举一个简短的例子,并说明enable_if 是如何做到的吗?
  • @R.MartinhoFernandes 我认为解释这些要点的单独答案可能会增加 OP 的价值。 :-) 顺便说一句,我认为编写像 is_f_able 这样的特征是图书馆作者的一项任务,他们当然可以使用 SFINAE,因为这会给他们带来优势,但对于“普通”用户并给出 is_f_able 的特征,我认为标签调度更容易。
  • @hansmaad 我发布了一个简短的答案来解决您的问题,并将在博客文章中解决“到 SFINAE 或不到 SFINAE”的问题(这个问题有点离题) .我的意思是,只要我有时间完成它。
  • SFINAE“脆弱”?什么?
【解决方案3】:

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

选项1:模板参数中的enable_if

  • 在构造函数中可用。

  • 可用于自定义转换运算符。

  • 它需要 C++11 或更高版本。

  • 在我看来,它更具可读性。

  • 重载容易误用和产生错误:

    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&lt;cond&gt; 而不是正确的std::enable_if_t&lt;cond, int&gt;::type = 0

选项 2:enable_if 在返回类型中

  • 它不能与构造函数(没有返回类型)一起使用
  • 不能用于用户定义的转换运算符(因为它不可推导)
  • 可以在 C++11 之前使用。
  • 第二个更易读的 IMO。

选项 3:enable_if 在函数参数中

  • 可以使用 pre-C++11。
  • 它可用于构造函数。
  • 它不能用于用户定义的转换运算符(它们没有参数)
  • 它不能用于具有固定数量参数的方法中,例如一元/二元运算符+-* 等。
  • 在继承中使用它是安全的(见下文)。
  • 改变函数签名(你基本上有一个额外的作为最后一个参数void* = nullptr);这会导致指针指向函数的行为不同等等。

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

继承和using有细微差别:

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

namespace.udecl

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

...

当 using-declarator 将基类中的声明引入派生类时,派生类中的成员函数和成员函数模板会覆盖和/或隐藏同名的成员函数和成员函数模板,参数-基类中的类型列表、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() {}
};

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

【讨论】:

    【解决方案4】:

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

    当问题被问到时,&lt;type_traits&gt; 中的std::enable_if 是可用的最佳工具,其他答案在 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; }   
    };
    

    【讨论】:

      猜你喜欢
      • 2011-07-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-03-13
      • 2011-03-10
      • 2011-04-16
      • 1970-01-01
      • 2010-09-29
      相关资源
      最近更新 更多