无用知识研究:用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

//////////////////////////////////////////////////////////////////////

额外添加,还有一种方案:

cpp 复制代码
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 deleted
    4 | void foo() = delete;
      |      ^
1 error generated.

//////////////////////////////////////////////////////////////////

//////////////////////////////////////////////////////////////////

帮助理解:Remastered enable_if

注意这句话:template里的typanme修饰的名字,只是描述性的。这句话的意思应该是这个名字不参与函数的signature的生成。

//////////////////////////////////////////////////////////////////

版本1:

cpp 复制代码
#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:

cpp 复制代码
#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;
}

另外一个例子:

cpp 复制代码
namespace detail {
    //这是一个最通用的模板,op也是一个模板,它接受类型T
    //然后靠Op<T>来对is_detected进行偏特化
    //如果Op<T>合理,那么is_detected就拥有了true
 
    template <
        typename T, //类名
        template <typename U> typename Op, //某种规则,对T进行某种分析
        typename = void
    >
        struct Detect : std::false_type {
    };
 
    //此版本为偏特化,毕竟std::void_t的结果就是void
    template <
        typename T, //类名
        template <typename U> typename Op //某种规则,对T进行某种分析
    >
        struct Detect<T, Op, std::void_t<Op<T>>> : std::true_type {
    };
 
} // namespace detail
 
namespace 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 detail
 
template <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++

cpp 复制代码
对于如下的代码,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.

With

template <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 solution

template <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.
cpp 复制代码
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

cpp 复制代码
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 fine
    foo<int>();

    // ERROR: subsitution failure, as expected
    // foo<double>();

    // Oops! also works, even though T != int :(
    foo<double, double>();

    return 0;
}
相关推荐
-森屿安年-2 小时前
数据结构——排序
数据结构·算法·排序算法
西望云天2 小时前
Trie树实战:三道典型例题
数据结构·算法·icpc
dragoooon342 小时前
[优选算法专题三.二分查找——NO.20搜索插入位置 ]
算法·leetcode·动态规划
宛 禾2 小时前
list的学习
c++·学习
FFZero12 小时前
积加科技音视频一面
c++·科技·音视频
hn小菜鸡3 小时前
LeetCode 1089.复写零
算法·leetcode·职场和发展
与己斗其乐无穷3 小时前
算法(一)双指针法
数据结构·算法·排序算法
艾莉丝努力练剑3 小时前
【编码表 && STL】C++编程基石:从字符编码表到STL标准库的完整入门指南
java·linux·c++
工头阿乐3 小时前
Ubuntu 安装与使用C++ onnxruntime库
linux·c++·ubuntu