无用知识研究:用sfinae实现函数模板的overload [一]
思考,sfinae全称为:"Substitution Failure Is Not An Error"。那么如何才能造成这个失败呢。这个应该是关键点所在。
/////////////////////////////////////////////////////////
省流:用enable_if_t<condition, int> = 0
而不是 typename = enable_if_t<condition>
Why should I avoid std::enable_if in function signatures
//////////////////////////////////////////////////////////////////////
额外添加,还有一种方案:
template <typename T>
void f() = delete;template <>
void f<int>() { }template <>
void f<double>() { }If the template is instantiated for any type besides int and double you get the following error message<source>:18:5: error: call to deleted function 'foo'18 | foo<char>();| ^~~~~~~~~
<source>:4:6: note: candidate function [with T = char] has been explicitly deleted4 | void foo() = delete;| ^
1 error generated.
//////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////
帮助理解:Remastered enable_if
注意这句话:template里的typanme修饰的名字,只是描述性的。这句话的意思应该是这个名字不参与函数的signature的生成。
//////////////////////////////////////////////////////////////////
版本1:
#include <cstdlib>
#include <type_traits>
#include <iostream>template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo()
{std::cout << "method 1" << std::endl;
}template <class T, std::enable_if_t<std::is_same_v<T, double>>* = 0>
void foo()
{std::cout << "method 2" << std::endl;
}int main()
{foo<int>();foo<double>();std::cout << "Done...";std::getchar();return EXIT_SUCCESS;
}输出:
method 1
method 2
Done...
版本2:
#include <iostream>#include <cstdlib>
#include <type_traits>
#include <iostream>template <class T, std::enable_if_t<std::is_same_v<T, int>,bool> = true>
void foo(T t)
{std::cout << "method 1" << std::endl;
}template <class T, std::enable_if_t<std::is_same_v<T, double>, bool> = true>
void foo(T t)
{std::cout << "method 2" << std::endl;
}int main()
{foo(1);foo(2.0);std::cout << "Done...";std::getchar();return EXIT_SUCCESS;
}
另外一个例子:
namespace detail {//这是一个最通用的模板,op也是一个模板,它接受类型T//然后靠Op<T>来对is_detected进行偏特化//如果Op<T>合理,那么is_detected就拥有了truetemplate <typename T, //类名template <typename U> typename Op, //某种规则,对T进行某种分析typename = void>struct Detect : std::false_type {};//此版本为偏特化,毕竟std::void_t的结果就是voidtemplate <typename T, //类名template <typename U> typename Op //某种规则,对T进行某种分析>struct Detect<T, Op, std::void_t<Op<T>>> : std::true_type {};} // namespace detailnamespace detail {//has_foo是个“类型”template <class U>using has_foo = decltype(std::declval<U>().foo());//has_update是个“类型”template <class U> /*这个U在使用时,其实是类名,类里可能会有foo这个函数*/using has_update = decltype(std::declval<U>().update(std::declval<float>()));} // namespace detailtemplate <typename T, template <typename> typename Op>
using is_detected = detail::Detect<T, Op>;// 调用函数的模板
template<typename T,std::enable_if_t<is_detected<T, detail::has_foo>::value, bool> = true>
bool call_foo(T& obj) {std::cout << "调用foo()函数: ";return obj.foo();
}
// 当没有foo()函数时的重载
template<typename T, std::enable_if_t < !is_detected<T, detail::has_foo>::value, bool> = true>
bool call_foo(T& obj) {std::cout << "类中没有foo()函数" << std::endl;return true;
}/*
这个是返回值类型的sfinae// 调用函数的模板
template<typename T>
typename std::enable_if<is_detected<T, detail::has_foo>::value,bool>::type
call_foo(T& obj) {std::cout << "调用foo()函数: ";return obj.foo();
}// 当没有foo()函数时的重载
template<typename T>
typename std::enable_if<!is_detected<T, detail::has_foo>::value, bool>::type
call_foo(T& obj) {std::cout << "类中没有foo()函数" << std::endl;return true;
}*/template<typename T>
bool Test(T& t)
{if (is_detected<T, detail::has_foo>::value){//t.foo();if (!call_foo(t))return false;}return true;
}struct xxx
{bool foo(){return true;}
};
struct yyy
{
};int fun123() {{xxx o;Test(o);}{yyy o;Test(o);}
}int main()
{fun123();return 1;
}
参考文章
Approaches to function SFINAE in C++
对于如下的代码,method 1和method 2,更推荐哪个呢,
有人推荐第二种:#include <cstdlib>
#include <type_traits>
#include <iostream>template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo()
{std::cout << "method 1" << std::endl;
}template <class T, std::enable_if_t<std::is_same_v<T, double>>* = 0>
void foo()
{std::cout << "method 2" << std::endl;
}int main()
{foo<int>();foo<double>();std::cout << "Done...";std::getchar();return EXIT_SUCCESS;
}输出:
method 1
method 2
Done...Suggestion: prefer method 2.Both methods work with single functions. The problem arises when you have more than a function, with the same signature, and you want enable only one function of the set.Suppose that you want enable foo(), version 1, when bar<T>() (pretend it's a constexpr function) is true, and foo(), version 2, when bar<T>() is false.Withtemplate <typename T, typename = std::enable_if_t<true == bar<T>()>>
void foo () // version 1{ }template <typename T, typename = std::enable_if_t<false == bar<T>()>>
void foo () // version 2{ }you get a compilation error because you have an ambiguity: two foo() functions with the same signature (a default template parameter doesn't change the signature).But the following solutiontemplate <typename T, std::enable_if_t<true == bar<T>(), bool> = true>
void foo () // version 1{ }template <typename T, std::enable_if_t<false == bar<T>(), bool> = true>
void foo () // version 2{ }works, because SFINAE modify the signature of the functions.Unrelated observation: there is also a third method: enable/disable the return type (except for class/struct constructors, obviously)template <typename T>
std::enable_if_t<true == bar<T>()> foo () // version 1{ }template <typename T>
std::enable_if_t<false == bar<T>()> foo () // version 2{ }As method 2, method 3 is compatible with selection of alternative functions with same signature.
Eric
Over a year ago
"a default template parameter doesn't change the signature" - how is this different in your second variant, which also uses default template parameters?max66
@Eric - Non simple to say... I suppose the other answer explain this better... If SFINAE enable/disable the default template argument, the foo() function remain available when you call it with an explicit second template parameter (the foo<double, double>(); call). And if remain available, there is an ambiguity with the other version. With method 2, SFINAE enable/disable the second argument, not the default parameter. So you can't call it explicating the parameter because there is a substitution failure that doesn't permit a second parameter. So the version is unavailable, so no ambiguity
ai翻译:很难简单说清楚…… 我认为另一个答案对此解释得更清楚…… 如果 SFINAE(替换失败并非错误)启用或禁用了默认模板参数,那么当你使用显式的第二个模板参数调用 foo () 函数时(即调用 foo<double, double>();),该函数仍然是可用的。而如果它仍然可用,就会与另一个版本的 foo () 函数产生歧义。
对于方法 2,SFINAE 启用或禁用的是第二个参数,而非默认参数。因此,你无法通过显式指定该参数来调用函数,因为此时会发生替换失败,导致无法使用第二个参数。这样一来,这个版本的函数就不可用了,也就不会产生歧义。
(说明:SFINAE 是 C++ 模板元编程中的核心概念,全称为 "Substitution Failure Is Not An Error",指在模板参数替换过程中,若某一替换导致错误,编译器不会将其视为编译错误,而是会忽略该模板重载,继续查找其他可行的重载版本。)Deduplicator
Method 3 has the additional advantage of generally not leaking into the symbol-name. The variant auto foo() -> std::enable_if_t<...> is often useful to avoid hiding the function-signature and to allow using the function-arguments.Eric
@max66: so the key point is that substitution failure in a template parameter default is not an error if the parameter is supplied and no default is needed?max66
@Eric - yes... seems to me that you caught the point: a substitution failure for a template parameter default disables only the calls of the function that don't express the parameter but maintain available the function itself. Method 2 disable that function itself.
ai翻译:
是的…… 在我看来,你抓住了关键:模板参数默认值的替换失败,只会禁用那些没有显式指定该参数的函数调用,而函数本身仍然是可用的。而方法 2 则会直接禁用函数本身。
(补充说明:此处延续了前文关于 C++ 模板 SFINAE 机制的讨论。“substitution failure” 即 “替换失败”,是 SFINAE 的核心场景 —— 当模板参数替换过程中出现错误时,编译器不会报错,而是会排除该模板重载。前半句描述的是 “默认模板参数替换失败” 的影响范围(仅限制无显式参数的调用),后半句则对比了 “方法 2” 的作用效果(直接让函数不可用),二者的核心区别在于对 “函数本身可用性” 的影响不同。)Vishal Subramanyam
Nov 8, 2024 at 10:30
Is there a method that achieves this using concepts or "requires" expressions?
针对max66的例子,关于为什么最好选method 2的方案,这个叫alter-igel的用户也追加了一个思路。可以看看其各种关于模板的回答。
https://stackoverflow.com/users/5023438/alter-igel
In addition to max66's answer, another reason to prefer method 2 is that with method 1, you can (accidentally) pass an explicit type parameter as the second template argument and defeat the SFINAE mechanism completely. This could happen as a typo, copy/paste error, or as an oversight in a larger template mechanism.
ai翻译:
除了 max66 的回答之外,更倾向于选择方法 2 的另一个原因是:使用方法 1 时,你可能会(无意间)将一个显式类型参数作为第二个模板参数传入,从而完全破坏 SFINAE 机制。这种情况可能源于拼写错误、复制粘贴失误,或是在更复杂的模板机制中因疏忽而导致。
(补充说明:此处 “defeat the SFINAE mechanism” 直译为 “破坏 SFINAE 机制”,结合上下文指原本依靠 SFINAE 实现的 “筛选有效模板重载” 功能失效 —— 由于显式传入参数绕过了默认参数的替换过程,SFINAE 无法发挥作用,可能导致本应被排除的无效模板重载被错误启用。)#include <cstdlib>
#include <type_traits>
#include <iostream>// NOTE: foo should only accept T=int
template <class T, class = std::enable_if_t<std::is_same_v<T, int>>>
void foo(){std::cout << "method 1" << std::endl;
}int main(){// works finefoo<int>();// ERROR: subsitution failure, as expected// foo<double>();// Oops! also works, even though T != int :(foo<double, double>();return 0;
}