174

Scott Meyers 发布了他的下一本书 EC++11 的内容和状态。他写道,书中的一项内容可能是“Avoid std::enable_ifin function signatures”

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

4 回答 4

114

将 hack 放入模板参数中。

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

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

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

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

于 2013-01-31T10:42:28.443 回答
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某种方式,但对于大多数常规使用的编译时条件,不建议这样做。

于 2013-01-30T09:33:52.313 回答
8

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

选项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<cond>而不是正确的std::enable_if_t<cond, int>::type = 0

选项 2:enable_if在返回类型中

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

选项 3:enable_if在函数参数中

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

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

继承和 存在细微差别using

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

命名空间.udecl

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

...

当 using-declarator 将基类中的声明带入派生类时,派生类中的成员函数和成员函数模板会覆盖和/或隐藏具有相同名称的成员函数和成员函数模板,参数类型列表,cv-基类中的限定和引用限定符(如果有)(而不是冲突)。此类隐藏或覆盖的声明被排除在 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) {}
};

演示

于 2018-03-28T10:56:03.450 回答
0

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

当问题被问到时,std::enable_iffrom<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; }   
};
于 2021-04-16T14:47:28.540 回答