为什么要在函数签名中避免 std: : able _ if

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将使用作为返回类型(这不是正常函数签名的一部分,而是模板专门化的一部分) ?
  • 成员函数模板和非成员函数模板有什么区别吗?
39876 次浏览

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

标记分派并不操作重载集,而是通过编译时表达式(例如,在类型 trait 中)提供适当的参数,帮助您精确地选择所需的函数。根据我的经验,这更容易调试和获得正确的结果。如果您是一个有抱负的具有复杂类型特征的库编写者,那么您可能会以某种方式需要 enable_if,但是对于大多数编译时条件的常规使用,不推荐使用 enable_if

将 hack 放入模板参数 中。

关于模板参数的 enable_if方法至少有两个优点:

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

  • 通用适用性 : 构造函数没有返回类型,一些操作符不能有额外的参数,因此其他两个选项都不能在任何地方应用。在模板参数中放入 able _ if 可以在任何地方工作,因为无论如何都只能在模板上使用 SFINAE。

对我来说,可读性方面是这个选择中最大的激励因素。

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

选项1: 模板参数中的 enable_if

  • 它在构造函数中是可用的。

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

  • 它需要 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_if_t<cond>而不是正确的 std::enable_if_t<cond, int>::type = 0

选项2: 返回类型中的 enable_if

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

选项3: 函数参数中的 enable_if

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

方案4(C + + 20) requires

现在有 requires条款

  • 它在构造函数中是可用的

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

  • 它需要 C + + 20

  • 在我看来,这是最容易读懂的

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

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

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

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

遗传和 using有细微的差别:

根据 using-declarator(强调地雷) :

Namespace.udecl

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

...

当使用声明器将基类的声明带入派生类时,派生类中的成员函数和成员函数模板将覆盖和/或隐藏成员函数和成员函数模板 在基类中使用相同的名称、参数类型列表、 cv 限定符和 ref 限定符(如果有的话)(而不是冲突)。这些隐藏的或重写的声明被排除在 using-Declaration ator 引入的声明集之外。

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

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

演示

以及 requires:

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

演示

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

当问到这个问题时,来自 <type_traits>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; }
};