void_t
源码分析和常规范例
功能:能够检测到应用SFINZE(替换失败并不是一个错误)特性出现的非类类型,换句话来说,给进来的类型必须是一个有效类型
判断类中是否有类型别名
namespace nmsp1{
struct NoInnerType
{
int m_i;
};
struct HaveInnerType
{
using type = int; //类型别名
void myfunc(){}
};
//泛化版本
template<typename T,typename U =std::void_t<> >
struct HasTypeMen : std::false_type //struct 默认public继承,class 默认私有继承
{
};
//特化版本
template<typename T>
struct HasTypeMen<T,std::void_t<typename T::type>> : std::true_type
{
};
//想要灵活可以用宏,反斜杠表示下一行是本行的一部分,##是连接字符串
#define _HAS_TYPE_MEN_(parMTpMn) \
template<typename T,typename U =std::void_t<> > \
struct HTM_##parMTpMn :std::false_type {}; \
template<typename T> \
struct HTM_##parMTpMn<T,std::void_t<typename T::parMTpNm>>:std::true_type{};
_HAS_TYPE_MEN_(type);
_HAS_TYPE_MEN_(sizetype);
}
cout << nmsp1::HasTypeMen<nmsp1::NoInnerType>::value << endl;
cout << nmsp1::HasTypeMen<nmsp1::HaveInnerType>::value << endl;
//根据SFINZE原则,特化版本被忽略,泛化版本更适合
cout << nmsp1::HTM_type<nmsp1::NoInnerType>::value << endl;//0
cout << nmsp1::HTM_type<nmsp1::HaveInnerType>::value << endl;//1
cout << nmsp1::HTM_sizetype<nmsp1::NoInnerType>::value << endl;//0
cout << nmsp1::HTM_sizetype<nmsp1::HaveInnerType>::value << endl;//0
判断类中是否有成员变量
//泛化版本
template<typename T, typename U = std::void_t<> >
struct HasMember : std::false_type //struct 默认public继承,class 默认私有继承
{
};
//特化版本
template<typename T>
struct HasMember<T, std::void_t<decltype(T::m_i)>> : std::true_type
{
};
判断类中是否成员函数的写法
//泛化版本
template<typename T, typename U = std::void_t<> >
struct HasMemFunc : std::false_type //struct 默认public继承,class 默认私有继承
{
};
//特化版本
template<typename T>
struct HasMemFunc<T, std::void_t<decltype(std::declval<T>().myfunc())>> : std::true_type
{
};
泛化版本和特化版本如何选择的问题
编译器通过一种复杂的排序规则来决定使用类模板的泛化版本还是特化版本,一般来说,void跟其他任何类型比,都是不受待见的
在查看后void_t的实际类型是void,那么我们能否直接替换它呢
//泛化版本
template<typename T,typename U =int > //注意此处
struct HasTypeMen : std::false_type //struct 默认public继承,class 默认私有继承
{
};
//特化版本
template<typename T>
struct HasTypeMen<T,std::void_t<typename T::type>> : std::true_type
{
};
cout << nmsp1::HasTypeMen<nmsp1::HaveInnerType>::value << endl;//0
编译器会认为泛化版本比下面的特化版本更适合
借助void_t和declval实现is_copy_assignable
is_copy_assignable:C++标准库的类模板,用来判断一个类对象是否能够拷贝赋值
namespace nmsp1 {
class ACLABL {
};
class BCPABL {
public:
//拷贝运算符
BCPABL& operator=(const BCPABL& tmpobj) {
//...
return *this;
}
};
class CCPABL {
public:
//拷贝运算符被标记为delete
CCPABL& operator=(const CCPABL& tmpobj) = delete;
};
}
nmsp1::ACLABL aobj1;
nmsp1::ACLABL aobj2;
aobj1 = aobj2;//拷贝赋值
nmsp1::BCPABL bobj1;
nmsp1::BCPABL bobj2;
bobj1 = bobj2;
nmsp1::CCPABL cobj1;
nmsp1::CCPABL cobj2;
cobj1 = cobj2;//error:无法引用拷贝函数
查看is_copy_assignable源码
仿照自己实现一个is_copy_assignable
namespace nmsp1{
//....
//泛化版本IsCopyAssignable类模板
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
{
//第二个declval应该返回const修饰的左值引用
};
}
cout << "int:" << std::is_copy_assignable<int>::value << endl;//1
cout << "ACLABL:" << std::is_copy_assignable<nmsp1::ACLABL>::value << endl;//1
cout << "BCPABL:" << std::is_copy_assignable<nmsp1::BCPABL>::value << endl;//1
cout << "BCPABL:" << std::is_copy_assignable<nmsp1::CCPABL>::value << endl;//1
cout << "int:" <<nmsp1::IsCopyAssignable<int>::value << endl;//1
cout << "ACLABL:" << nmsp1::IsCopyAssignable<nmsp1::ACLABL>::value << endl;//1
cout << "BCPABL:" << nmsp1::IsCopyAssignable<nmsp1::BCPABL>::value << endl;//1
cout << "BCPABL:" << nmsp1::IsCopyAssignable<nmsp1::CCPABL>::value << endl;//0
从上面实现看,左边的左值引用有点像拷贝函数的返回类型,而右边的带const修饰的左值引用有点像拷贝函数内的const参数。因为替换失败并不是一个错误这一特性的存在,当发现类型之间不能拷贝时候,编译器会去更适合的泛型版本,那么value也随之变为false
值得注意的时候左边declval不能是一个普通的T,如果是普通的T,那么对于内置类型int来说,下面的结果会返回0,代表不能被拷贝赋值,显然这一信息是错误的,当传入的是int,会返回的是其右值引用,而等号的左边是不能为右值引用类型,由于其替换失败并不是一个错误这一特性的存在,编译器转而去实现泛化版本
cout << "int() : " << std::is_copy_assignable<int()>::value << endl;//0
cout << "int():" << nmsp1::IsCopyAssignable<int()>::value << endl; //0
综合案例
假设我们有两个容器vector,元素数量都相同,但是这两个容器中的类型不同,假设第一个是int,第二个是double类型,希望重载一下+运算符,做一下加法运算,加法运算的意思是一个容器的第一个元素与第二个容器的第一个元素相加
//version:1
namespace nmsp2{
//VecAddResult_t<T,U>
template<typename T,typename U>
std::vector<T> operator+(std::vector<T> const& vecl1, std::vector<U> const& vec2) {}
}
如果这样实现,那么返回的类型真的是正确嘛,永远以第一个参数T作为容器的参数返回,如果
int + double
正确应该是返回的double,而在这段代码中他把第一个参数int作为容器的类型返回,这样的感觉不是很好。那么在设计上返回类型我们是否可以让编译器推导去返回一个更好的类型
//version:2
namespace nmsp2{
//考虑设计一个类模板VecAddResult。
template<typename T, typename U>
struct VecAddResult
{
using type = decltype(T()+ U());//把结果类型推导交给编译器
};//如果容器的类型不是基本类型,而是类类型,需要重载加法运算符
template<typename T,typename U>
std::vector<typename VecAddResult<T,U>::type> operator+(std::vector<T> const& vecl1, std::vector<U> const& vec2) {
std::vector<VecAddResult_t<T, U>> tmpvec;
//...
return tmpvec;
}
}
//上面T() + U():类似,内置和类类型都可以
int i = int(); //定义int类型的变量,而且这种定义方式会把i的初值设置为0
i = 5;
double j = double();//定义double类型的变量,而且这种定义方式会把i的初值设置为0.0
j = 13.6;
这样写法虽然可以让编译器去进行推导返回的类型,但此时又有一个新的问题出现了,那就是如果我vector里面的类型是类类型,且这个类类型有一个接收参数的构造函数,(在有参构造函数下,编译器无法为这类类型创建默认构造函数)或者默认构造函数为私有权限,那么此时编译会错误,首先在上面VecAddResult的写法中,在decltype内进行推导出来时,是需要实例化里面变量生成右值,也就是临时变量。
struct elemC
{
//如果写一个构造函数
elemC(int tmp);//带一个参数的构造函数,会报错,没有合适的默认构造函数
elemC operator+(const elemC& tmppar) {
}
};
std::vector<int> veca1;
std::vector<double> vecb1;
//veca1 + vecb1;//因为重载+法运算符在nmsp2空间内
nmsp2::operator+(veca1, vecb1);
std::vector<nmsp2::elemC> veca2;
std::vector<nmsp2::elemC> vecb2;
nmsp2::operator+(veca2, vecb2);//error:没有合适的构造函数
//此时定位错误在VecAddResult中using type = decltype(T()+ U());这一行
这一情况下也挺好解决,可以采用之前介绍到的std::declval假想创建并不是真正实例对象的方法
//新版VecAddResult
struct VecAddResult
{
//using type = decltype(T()+ U());//把结果类型推导交给编译器
using type = decltype(std::declval<T>() + std::declval<U>());
};
现在我们把类类型的重载的加法删掉
struct elemC
{
elemC(int tmp);
//...
};
nmsp2::operator+(veca2, vecb2);//这一行报未找到重载的+函数
//另一个error为:二进制"+":"T"不定义该运算符或到预定义运算符可接收的类型的转换
//把错误定位到VecAddResult中
//using type = decltype(std::declval<T>() + std::declval<U>());上
此时因为类类型没有重载加法运算符,而编译器进行报错,观看上面案例,我们发现这种报错不是我们想到的,出现的位置不是那么明朗。
可以考虑希望通过SFINAE特性检测一下两个类型的对象之间到底能不能相加
namespace nmsp2{
//泛化版本
template<typename T,typename U,typename V = std::void_t<>>
struct IfCanAdd : std::false_type
{
};
//特化版本
template<typename T, typename U>
struct IfCanAdd<T,U,std::void_t<decltype(std::declval<T>()+std::declval<U>())>> : std::true_type
{
};
//泛化版本的VecAddResult
template<typename T, typename U ,bool ifcando = IfCanAdd<T,U>::value>
struct VecAddResult {
using type = decltype(std::declval<T>() + std::declval<U>());
};
template<typename T, typename U>
struct VecAddResult<T,U,false> {
};
template<typename T, typename U>
using VecAddResult_t = typename VecAddResult<T, U>::type;
template<typename T,typename U>
std::vector<typename VecAddResult<T,U>::type> operator+(std::vector<T> const& vecl1, std::vector<U> const& vec2) {
std::vector<VecAddResult_t<T, U>> tmpvec;
//...
return tmpvec;
}
}
std::vector<nmsp2::elemC> veca2;
std::vector<nmsp2::elemC> vecb2;
nmsp2::operator+(veca2, vecb2);
当类类型没有重载运算符时,因为替换失败并不是一个错误这一特性的存在,所以编译器转而去执行IfCanAdd : std::false_type,那么其value置为0,就会去执行VecAddResult<T,U,false>,这个模板内没有别名type,那么又由于替换失败并不是一个错误这一特性,operator+函数模板被忽略了。
编写模板时的规则:如果选择要实例化某个模板(operator+,VecAddResult),则实例化时不应该失败(编译错误)
VecAddResult和IfCanAdd这两个模板之间有一种SFINAE-friendly(SFINAE友好)关系