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


165

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

作为模板参数:

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使用函数签名”与返回类型的使用有关(这不是正常函数签名的一部分,而是模板专业化的一部分)?
  • 成员函数模板和非成员函数模板有什么区别吗?

通常,因为重载同样好。如果有的话,委托给使用(专用)类模板的实现。
sehe 2013年

成员函数的不同之处在于,过载集包括在当前过载之后声明的过载。这在执行variadics延迟返回类型(从另一个重载推断返回类型)时特别重要
13年

1
好吧,只是主观上我不得不说,尽管经常很有用,但我不想std::enable_if使我的函数签名混乱(尤其是丑陋的附加nullptr函数参数版本),因为它看起来总是像是一个奇怪的hack(对于某种static if可能做得更漂亮,更干净)使用black-magic模板来开发有趣的语言功能。这就是为什么我更喜欢标签分发的原因(嗯,您仍然有其他奇怪的参数,但不在公共接口中,而且也不太丑陋和含糊不清)。
Christian Rau

2
我要问什么=0typename std::enable_if<std::is_same<U, int>::value, int>::type = 0完成?我找不到正确的资源来理解它。我所知道的第一部分之前=0有一个成员类型int,如果Uint是一样的。非常感谢!
astroboylrx

4
@astroboylrx有趣的是,我只是要发表一条评论来指出这一点。基本上,= 0表示这是默认的非类型模板参数。之所以这样做,是因为默认类型模板参数不是签名的一部分,因此您不能对其进行重载。
尼尔·弗里德曼

Answers:


107

将hack放到模板参数中

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

  • 可读性:enable_if用法和return / argument类型不会合并到一个混乱的typename歧义块和嵌套的类型访问中;即使可以使用别名模板减轻歧义词和嵌套类型的混乱情况,但仍会将两个无关的东西合并在一起。enable_if的使用与模板参数有关,与返回类型无关。将它们包含在模板参数中意味着它们更接近要紧的内容。

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

对我而言,可读性是此选择的主要推动因素。


4
这里使用FUNCTION_REQUIRES宏,使其更易于阅读,并且也可以在C ++ 03编译器中使用,并且依赖于在返回类型中使用。同样,在函数模板中使用参数会导致重载问题,因为现在函数签名不是唯一的,从而导致模棱两可的重载错误。enable_ifenable_if
Paul Fultz II

3
这是一个古老的问题,但对于仍在阅读的任何人:@Paul提出的问题的解决方案是使用enable_if默认的非类型模板参数,该参数允许重载。即enable_if_t<condition, int> = 0代替typename = enable_if_t<condition>
尼尔·弗里德曼

向几乎静态的if的
回溯

@ R.MartinhoFernandes flamingdangerzone您评论中的链接现在似乎指向间谍软件安装页面。我标记了它以引起主持人注意。
nispio

58

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 */) {}
}

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


22
但是,标记分派有一个缺点:如果您具有某种特征来检测功能的存在,并且该功能是通过标记分派方法实现的,则它始终报告该成员存在,并导致错误而不是潜在的替换失败。 。SFINAE主要是一种用于从候选集中消除重载的技术,而标记调度是一种用于在两个(或多个)重载之间进行选择的技术。功能上有一些重叠,但并不等同。
R. Martinho Fernandes

@ R.MartinhoFernandes您可以举一个简短的例子,并说明如何enable_if正确处理吗?
TemplateRex

1
@ R.MartinhoFernandes我认为一个单独的解释这些问题的答案可能会增加OP的价值。:-)顺便说一句,写特征像is_f_able是我认为对于图书馆作家来说是一项任务,他们当然可以使用SFINAE,这可以给他们带来好处,但是对于“常规”用户并赋予特质is_f_able,我认为标签分发更容易。
TemplateRex

1
@hansmaad我针对您的问题发布了一个简短的答案,并将在博客文章中解决“发给SFINAE或不发给SFINAE”的问题(此问题有些偏离主题)。我的意思是,只要我有时间完成它。
R. Martinho Fernandes 2013年

8
SFINAE是“脆弱”的吗?什么?
Lightness Races in Orbit

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()

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

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

演示版

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.