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
函数签名”关注用法作为返回类型(不是正常函数签名的一部分,而是模板特化的一部分)?答案 0 :(得分:102)
将hack放入模板参数。
模板参数方法enable_if
至少比其他方法有两个优势:
可读性:enable_if use和return / argument类型没有合并为一个混乱的typename disambiguators和嵌套类型访问块;即使可以使用别名模板来缓解消歧器和嵌套类型的混乱,但仍然会将两个不相关的事物合并在一起。 enable_if use与模板参数有关,而与返回类型无关。将它们放在模板参数中意味着它们更接近重要的事项;
通用适用性:构造函数没有返回类型,并且某些运算符不能有额外的参数,因此其他两个选项都不能应用于任何地方。将enable_if放在模板参数中无处不在,因为无论如何只能在模板上使用SFINAE。
对我来说,可读性方面是这种选择的重要推动因素。
答案 1 :(得分:55)
std::enable_if
依赖于“Substition Failure Is Not An Error”(又名SFINAE)原则。这是非常脆弱的语言功能,您需要非常小心才能正确使用它。
enable_if
中的条件包含嵌套模板或类型定义(提示:查找::
标记),则这些嵌套模型或类型的解析通常为非推导出的背景。在这种未推断的上下文中,任何替换失败都是错误。enable_if
重载中的各种条件不能有任何重叠,因为重载决策是不明确的。这是作为作者需要自己检查的内容,尽管您会收到良好的编译器警告。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
某种方式,但是对于大多数经常使用的编译时条件,不建议这样做。
答案 2 :(得分:4)
应该首选哪种解决方案,为什么要避免使用其他解决方案?
模板参数
它可能很容易被错误地使用并产生过载错误:
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
返回类型:
最后,在函数参数中:
+
,-
,*
,...)void* = nullptr
)(因此函数指针会有所不同,等等)会员和非会员功能模板是否有任何差异?
与继承和using
:
根据using-declarator
(强调我的):
using-declarator引入的声明集是通过对using-declarator中的名称执行限定名查找([basic.lookup.qual],[class.member.lookup])来找到的,不包括函数隐藏如下所述。
...
当using-declarator将基类的声明带入派生类时,派生类中的成员函数和成员函数模板覆盖和/或隐藏成员函数和成员函数模板,其名称相同,参数 - 基类中的type-list,cv-qualification和ref-qualifier(如果有的话)(而不是冲突)。这些隐藏或重写的声明被排除在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) {}
};
答案 3 :(得分:0)
“应该首选哪种解决方案,为什么我应该避免使用其他解决方案?”
提出问题时,std::enable_if
中的 <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; }
};