【模板进阶】std::void_t

一、 s t d : : v o i d _ t std::void\_t std::void_t的源码分析和常规范例

1. s t d : : v o i d _ t 1.std::void\_t 1.std::void_t的源码分析

C + + 17 C++17 C++17引入了 s t d : : v o i d _ t std::void\_t std::void_t,它其实是一个别名模板,源码非常简单,大概如下所示:

//void_t的实现
template<typename... Args> //别名模板
using void_t = void; //无论传入什么void_t都是void

它实际上是一个别名模板,但有个特点,就是无论传入什么都会变成 v o i d void void类型。
通过它的这个特性,我们能够检测到应用 S F I N A E SFINAE SFINAE特性时出现的非法类型,也就是说,传入的类型必须是有效的类型,而不是非法类型。


2.常规范例

2.1 使用 s t d : : v o i d _ t std::void\_t std::void_t来判断类内是否有某个类型别名

考虑以下的代码:

//判断类中是否存在某个类型别名
struct NoInnerType {
   int m_i;
};

struct HaveInnerType {
    using type = int;
    void myfunc() {}
};

//泛化版本 
template<typename T,typename U = std::void_t<>>
struct HasTypeMem :std::false_type {  //继承false_type

};

//特化版本
template<typename T>
struct HasTypeMem <T, std::void_t<typename T::type>> :std::true_type { //继承true_type
    
};

void Test1() {

    //type成员是static constexpr(静态常量)
    std::cout << HasTypeMem<NoInnerType>::value << "\n"; //没有type成员,所以会调用泛化版本

    std::cout << HasTypeMem<HaveInnerType>::value << "\n"; //有type成员,所以会调用特化版本

}

这里我们的 H a s I n n e r T y p e HasInnerType HasInnerType内部有一个类型别名 t y p e type type,而 N o I n n e r T y p e NoInnerType NoInnerType内部没有类型别名。

H a s T y p e M e m HasTypeMem HasTypeMem是用于判断是否类内具有 t y p e type type类型的模板,其泛化版本继承了 s t d : : t r u e _ t y p e std::true\_type std::true_type,而特化版本继承了 s t d : : f a l s e _ t y p e std::false\_type std::false_type

因此我们在实例化 H a s T y p e M e m HasTypeMem HasTypeMem模板的时候,就可以调用其内部的 v a l u e value value静态变量来查看是否存在 t y p e type type这个类型别名。


重点是这个地方:
在这里插入图片描述因为我们在实例化 T T T类型为 H a s I n n e r T y p e HasInnerType HasInnerType类型时,编译器发现 H a s I n n e r T y p e HasInnerType HasInnerType类型内部的确有一个 t y p e type type类型,因此会实例化这个特化版本。
相反,实例 N o I n n e r T y p e NoInnerType NoInnerType模板时,由于无法实例化特化的模板,就会实例化泛化的模板了。


调用结果如下:
在这里插入图片描述


当然,如果你喜欢宏定义,也可以使用宏定义来取个别名,注意这里的"\"之后必须紧跟换行,而##用于连接宏参数:

#define _HAS_TYPE_MEM_(parMtpNm) \
template<typename T, typename = std::void_t<>> \
struct HTM_##parMtpNm : std::false_type{}; \
    \
    template<typename T> \
    struct HTM_##parMtpNm<T, std::void_t<typename T::parMtpNm>> : std::true_type {};

_HAS_TYPE_MEM_(type);
_HAS_TYPE_MEM_(sizetype);

void Test2() {
    std::cout << HTM_type<NoInnerType>::value << "\n";
    std::cout << HTM_type<HaveInnerType>::value << "\n"; //存在type名称的静态常量

    std::cout << HTM_sizetype<NoInnerType>::value << "\n";
    std::cout << HTM_sizetype<HaveInnerType>::value << "\n"; //不存在sizetype名称的静态常量

}

2.2 判断某个类中是否存在某个成员变量

类似地,我们可以借助 d e c l t y p e decltype decltype s t d : : v o i d t std::void_t std::voidt来判断是否一个类内具有某个成员变量:

//判断某个类中是否存在某个成员变量

//泛化版本
template<typename T, typename U = std::void_t<>>
struct HasMember :std::false_type {};

//特化版本
template<typename T>
struct HasMember<T,std::void_t<decltype(T::m_i)>> :std::true_type {};

void Test3() {
    std::cout << HasMember<NoInnerType>::value << "\n"; //存在成员变量,所以调用特化版本

    std::cout << HasMember<HaveInnerType>::value << "\n"; //不存在成员变量,调用了泛化版本
}

这里使用 d e c l t y p e decltype decltype来推导 T : : m _ i T::m\_i T::m_i的类型,如果存在这个名字的成员变量,就会实例化特化版本,运行结果如下:
在这里插入图片描述


2.3 判断类中是否存在某个成员函数

而成员函数如何推导为类型呢? 可以回顾之前学习的 d e c l v a l declval declvaldeclval的使用

我们这里使用 d e c l v a l declval declval来临时调用 T T T类型的 m y f u n c ( ) myfunc() myfunc()函数,实际上并没有调用,配合 d e c l t y p e decltype decltype我们可以轻松推导出这个成员函数的类型。参考下方代码:

//判断类中是否存在某个成员函数

//泛化版本
template<typename T,typename U = std::void_t<>>
struct HasMemFunc:std::false_type {};

//特化版本
template<typename T>
struct HasMemFunc<T, decltype(std::declval<T>().myfunc())> : std::true_type {};

void Test4() {
    std::cout << HasMemFunc<NoInnerType>::value << "\n"; //不存在myfunc函数,调用泛化版本

    std::cout << HasMemFunc<HaveInnerType>::value << "\n"; //存在myfunc函数,调用特化版本
}

二、编译器是选择特化类型还是泛化类型

通常我们认为,编译器会优先考虑特化版本,然后才是泛化版本。 的确,大部分情况下是如此的,但是编译器内部有它自己的一套排序方式,并不是绝对如此的。

考虑下方代码:


//编译器如何选择泛化版本还是特化版本

//泛化版本
template<typename T,typename U = int>
struct HasMember2 :std::false_type {};

//特化版本
template<typename T>
struct HasMember2<T, std::void_t<decltype(T::m_i)>> :std::true_type {};

void Test5() {
    std::cout << HasMember2<NoInnerType>::value << "\n"; //存在成员变量,但是仍然调用泛化版本!

    std::cout << HasMember2<HaveInnerType>::value << "\n"; //不存在成员变量,调用了泛化版本
}

这里我们的 N o I n n e r T y p e NoInnerType NoInnerType存在 m _ i m\_i m_i名称的成员类型,但是编译器仍然使用的泛化版本!如下:

在这里插入图片描述

因为在这里,编译器认为第二个参数为 i n t int int比通过 d e c l t y p e ( T : : m _ i ) decltype(T::m\_i) decltype(T::m_i)推导出来要合适,因此优先调用了泛化版本。

三、借助 v o i d _ t void\_t void_t d e c l v a l declval declval实现 i s _ c o p y _ a s s i g n a b l e is\_copy\_assignable is_copy_assignable

3.1 s t d : : i s _ c o p y _ a s s i g n a b l e std::is\_copy\_assignable std::is_copy_assignable的使用

s t d : : i s _ c o p y _ a s s i g n a b l e std::is\_copy\_assignable std::is_copy_assignable C + + C++ C++标准库中的一个类模板,用来判断一个类对象是否可以进行拷贝赋值的。

通常,一个空的类是可以进行拷贝赋值的,或者一个重载了拷贝赋值运算符的类是能够拷贝赋值的:

如下:

//std::is_copy_assignable判断一个类对象是否可以进行拷贝赋值

class ACPABL {

};

class BCPABL {
public:

	BCPABL & operator=(BCPABL const&other) { //赋值运算符
		return *this;
	}
};

当一个类显式的删除赋值运算符,这个类就不能被拷贝:

class CCPABL {
	CCPABL& operator=(CCPABL const& other) = delete; //删除赋值运算符
};

3.2实现 s t d : : i s _ c o p y _ a s s i g n a b l e std::is\_copy\_assignable std::is_copy_assignable

我们可以使用 v o i d _ t void\_t void_t d e c l v a l declval declval来自己实现一个相同功能的类,如果能够发生拷贝
赋值,那么就能满足 v o i d _ t < . . . > void\_t<...> void_t<...>内部的表达式,如下所示:

//实现is_copy_assignable

//泛化版本
template<typename T,typename U = std::void_t<>>
struct IsCopyAssignable :std::false_type {

};

//特化版本

template<typename T>
struct IsCopyAssignable<T, std::void_t<decltype(std::declval<T&>() = std::declval<const T&>())>>
	:std::true_type {//使用T&保证使用左值引用来接受赋值

};

void Test2() {
	std::cout << IsCopyAssignable<ACPABL>::value << "\n";
	std::cout << IsCopyAssignable<BCPABL>::value << "\n";//注意必须是拷贝运算符必须public下的
	std::cout << IsCopyAssignable<CCPABL>::value << "\n"; //删除了拷贝运算符无法赋值
	std::cout << IsCopyAssignable<int>::value << "\n";

}

可以发现,如果可以发生拷贝赋值,那么 d e c l t y p e ( s t d : : d e c l v a l < T & > ( ) = s t d : : d e c l v a l < c o n s t   T & > ( ) ) decltype(std::declval<T\&>() = std::declval<const \ T\&>()) decltype(std::declval<T&>()=std::declval<const T&>())一定能成立,那么就可以实例化出特化版本了。

注意这里需要使用 T & T\& T&来保证 d e c l v a l declval declval返回的一定是左值引用来接受赋值,而不是右值引用。


运行结果如下:

在这里插入图片描述
可以发现,显式删除拷贝赋值函数的类是无法被赋值的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值