C++惯用法: 通过std::decltype来SFINAE掉表达式

目录

1.什么是SFINAE

2.SFINAE(替换失败不是错误)

3.通过std::decltype来SFINAE掉表达式


1.什么是SFINAE

SFINAE 技术,即匹配失败不是错误,英文Substitution Failure Is Not An Error,其作用是当我们在进行模板特化的时候,会去选择那个正确的模板,避免失败。

SFINAE一般用于函数重载和编译期间类型检查,标准库中很多type traits模板就是通SFINAE来实现的。

看个具体的例子:

cpp 复制代码
#include <iostream>
#include <type_traits>
using namespace std;

template<typename T>
struct check_has_member_id
{
    // 仅当T是一个类类型时,"U::*"才是存在的,从而这个泛型函数的实例化才是可行的
    // 否则,就将触发SFINAE
    template<typename U>
    static void check(decltype(&U::id)){}

    // // 仅当触发SFINAE时,编译器才会"被迫"选择这个版本
    template<typename U>
    static int check(...){}

    enum {value = std::is_void<decltype(check<T>(NULL))>::value};
};

struct TEST_STRUCT
{
    int rid;
};

struct TEST_STRUCT2
{
    int id;
};

int main()
{
    check_has_member_id<TEST_STRUCT> t1;
    cout << t1.value << endl;

    check_has_member_id<TEST_STRUCT2> t2;
    cout << t2.value << endl;

    check_has_member_id<int> t3;
    cout << t3.value << endl;
    return 0;
}
// g++ --std=c++11  xxx.c

核心的代码是在实例化check_has_member_id对象的时候,通过模板参数T的类型,决定了结构体中对象value的值。而value的值是通过check<T>函数的返回值是否是void决定的。如果T中含有id成员的话,那么就会匹配第一个实例,返回void;如果不包含id的话,会匹配默认的实例,返回int。

利用这个机制还可以做很多类似的判断,比如判断一个类是否是结构体。

cpp 复制代码
#include <iostream>
#include <type_traits>

// 2. 判断变量是否是一个struct 或者 类
// https://www.jianshu.com/p/d09373b83f86
template <typename T>
struct check
{
    template <typename U>
    static void check_class(int U::*) {}

    template <typename U>
    static int check_class(...) {}

    enum { value = std::is_void<decltype(check_class<T>(0))>::value };
};

class myclass {};

int main()
{
    check<myclass> t;
    std::cout << t.value << std::endl;

    check<int> t2;
    std::cout << t2.value << std::endl;
    return 0;
}

std::is_void的用法可参考:

C++17之std::void_t-CSDN博客

2.SFINAE(替换失败不是错误)

在一个函数调用的备选方案中包含函数模板时,编译器首先要决定应该将什么样的模板参数 用于各种模板方案,然后用这些参数替换函数模板的参数列表以及返回类型,最后评估替换 后的函数模板和这个调用的匹配情况(就像常规函数一样)。

但是这一替换过程可能会遇到问题:替换产生的结果可能没有意义。不过这一类型的替换不会导致错误,C++语言规则要 求忽略掉这一类型的替换结果。

考虑如下的例子:

cpp 复制代码
// number of elements in a raw array:
template<typename T, unsigned N>
std::size_t len (T(&)[N])
{
    return N;
}
// number of elements for a type having size_type:
template<typename T>
typename T::size_type len (T const& t)
{
    return t.size();
}

当传递的参数是裸数组或者字符串常量时,只有那个为裸数组定义的函数模板能够匹配:

cpp 复制代码
int a[10];
std::cout << len(a); // OK: only len() for array matches
std::cout << len("tmp"); //OK: only len() 

如果只是从函数签名来看的话,对第二个函数模板也可以分别用 int[10]和 char const [4]替换 类型参数 T,但是这种替换在处理返回类型 T::size_type 时会导致错误。因此对于这两个调用, 第二个函数模板会被忽略掉。

如果传递的是裸指针,以上两个模板都不会被匹配上(但是不会因此而报错)。此时编译 期会抱怨说没有发现合适的 len()函数:

cpp 复制代码
int* p;
std::cout << len(p); // ERROR: no matching len() function found

但是这和传递一个有 size_type 成员但是没有 size()成员函数的情况不一样。比如如果传递的参数是 std::allocator<>:

cpp 复制代码
std::allocator<int> x;
std::cout << len(x); // ERROR: len() function found, but can't size()

此时编译器会匹配到第二个函数模板。因此不会报错说没有发现合适的 len()函数,而是会 报一个编译期错误说对 std::allocator而言 size()是一个无效调用。此时第二个模板函数不 会被忽略掉。

如果忽略掉那些在替换之后返回值类型为无效的备选项,那么编译器会选择另外一个参数类 型匹配相差的备选项。比如:

cpp 复制代码
// number of elements in a raw array:
template<typename T, unsigned N>
std::size_t len (T(&)[N])
{
    return N;
}
// number of elements for a type having size_type:
template<typename T>
typename T::size_type len (T const& t)
{
    return t.size();
 
}
// 对所有类型的应急选项:
std::size_t len (...)
{
    return 0;
}

此处额外提供了一个通用函数 len(),它总会匹配所有的调用,但是其匹配情况也总是所有 重载选项中最差的(通过省略号...匹配)。

对于指针,只有应急选项能够匹配上,此时编译器不会再报缺少适用 于本次调用的 len()。不过对于 std::allocator的调用,虽然第二个和第三个函数都能匹配 上,但是第二个函数依然是最佳匹配项。因此编译器依然会报错说缺少 size()成员函数。

3.通过std::decltype来SFINAE掉表达式

对于有些限制条件,并不总是很容易地就能找到并设计出合适的表达式来 SFINAE 掉函数模 板。

比如,对于有 size_type 成员但是没有 size()成员函数的参数类型,我们想要保证会忽略掉函 数模板 len()。如果没有在函数声明中以某种方式要求 size()成员函数必须存在,这个函数模 板就会被选择并在实例化过程中导致错误:

cpp 复制代码
template<typename T>
typename T::size_type len (T const& t)
{
 
return t.size();
}
std::allocator<int> x;
std::cout << len(x) << '\n'; //ERROR: len() selected, 

处理这种情况有一个常见的模式或者习惯用法:

1)通过尾置返回类型语法(函数名前用auto修饰,并在函数名后跟->,再加末尾的返回类型) 来制定返回类型。

2)使用std::decltype和逗号运算符来定义返回类型。

3)将所有必须成立的表达式放置于逗号运算符开头(表达式转换为void类型,以防逗号运算符重载)。

4)在逗号运算符末尾定义一个实际返回类型(类型为返回类型)的对象。

例如:

cpp 复制代码
template<typename T>
auto len (T const& t) -> decltype( (void)(t.size()), T::size_type() )
{
    return t.size();
}

这里返回类型定义为:

cpp 复制代码
decltype( (void)(t.size()), T::size_type() )

由于decltype构造的操作数是以逗号分隔的表达式列表,因此,最后一个表达式T::size_type()生成所需返回类型的值(decltype将其转换为返回类型)。(最后一个)逗号之前的表达式是必须成立的。在本例中就是t.size()。将表达式强制转换为void,是为了避免由于用户自定义重载表达式对于类型的逗号运算符而带来的问题。

请注意,decltype的实参是一个未求值的操作数。这意味着可以在不调用构造函数的情况下创建"虚对象",请参考:

C/C++中decltype关键字用法总结_c++ decltype用法-CSDN博客

相关推荐
C++忠实粉丝几秒前
计算机网络socket编程(4)_TCP socket API 详解
网络·数据结构·c++·网络协议·tcp/ip·计算机网络·算法
一个小坑货7 分钟前
Cargo Rust 的包管理器
开发语言·后端·rust
bluebonnet2711 分钟前
【Rust练习】22.HashMap
开发语言·后端·rust
古月居GYH12 分钟前
在C++上实现反射用法
java·开发语言·c++
Betty’s Sweet14 分钟前
[C++]:IO流
c++·文件·fstream·sstream·iostream
敲上瘾28 分钟前
操作系统的理解
linux·运维·服务器·c++·大模型·操作系统·aigc
福大大架构师每日一题30 分钟前
文心一言 VS 讯飞星火 VS chatgpt (396)-- 算法导论25.2 1题
算法·文心一言
不会写代码的ys34 分钟前
【类与对象】--对象之舞,类之华章,共绘C++之美
c++
兵哥工控37 分钟前
MFC工控项目实例三十二模拟量校正值添加修改删除
c++·mfc
在下不上天37 分钟前
Flume日志采集系统的部署,实现flume负载均衡,flume故障恢复
大数据·开发语言·python