一、 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 declval:declval的使用
我们这里使用 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返回的一定是左值引用来接受赋值,而不是右值引用。
运行结果如下:
可以发现,显式删除拷贝赋值函数的类是无法被赋值的。