01 一个实例:累加一个序列
1.1 Fixed Traits
#include <iostream>
template <typename T>
T accum (const T* beg, const T* end)
{
T total{};
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
int main()
{
char name[] = "templates";
int length = sizeof(name)-1;
std::cout << accum(name, name+length) / length << '\n'; // -5
}
- 上述代码的问题是,对于char类型希望计算对应ASCII码的平均值,但结果却是-5(预期结果是108),原因在于模板基于char类型实例化,结果出现了越界。多引入一个模板参数AccT来指定total的类型即可解决此问题,但每次调用都要指定这个类型
accum<int>(name, name+length)
- 另一个方法是为每个T类型创建一个关联类型,即T的traits,它表示total的类型
template<typename T>
struct Accumulationtraits;
template<>
struct Accumulationtraits<char> {
using AccT = int;
};
template<>
struct Accumulationtraits<short> {
using AccT = int;
};
template<>
struct Accumulationtraits<int> {
using AccT = long;
};
template<>
struct Accumulationtraits<unsigned int> {
using AccT = unsigned long;
};
template<>
struct Accumulationtraits<float> {
public:
using AccT = double;
};
template<typename T>
auto accum (const T* beg, const T* end)
{
using AccT = typename Accumulationtraits<T>::AccT;
AccT total{};
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
- 现在的输出就是预期结果了
1.2 Value Traits
- accum模板使用了默认构造函数的返回值初始化total,但不能保证返回值符合条件,类型AccT也不一定有一个默认构造函数。为了解决这个问题,需要添加一个value traits
template<typename T>
struct Accumulationtraits;
template<>
struct Accumulationtraits<char> {
using AccT = int;
static const AccT zero = 0;
};
template<>
struct Accumulationtraits<short> {
using AccT = int;
static const AccT zero = 0;
};
template<>
struct Accumulationtraits<int> {
using AccT = long;
static const AccT zero = 0;
};
template<typename T>
auto accum (const T* beg, const T* end)
{
using AccT = typename Accumulationtraits<T>::AccT;
AccT total = Accumulationtraits<T>::zero;
while (beg != end) {
total += *beg;
++beg;
}
return total;
}
- 但这种方法的缺点是,类内初始化的static成员变量只能是整型(int、long、unsigned)常量或枚举类型,如下面的特化就是错误的
template<>
struct Accumulationtraits<float> {
using Acct = float;
static constexpr float zero = 0.0f; // ERROR: not an integral type
};
- const和constexpr不允许这样初始化non-literal type,比如用户定义的任意精度的BigInt就可能不是一个literal type
class BigInt {
BigInt(long long);
...
};
template<>
struct Accumulationtraits<BigInt> {
using AccT = BigInt;
static constexpr BigInt zero = BigInt{0}; // ERROR: not a literal type
};
- 直接的解决方法是在类外定义value traits
template<>
struct Accumulationtraits<BigInt> {
using AccT = BigInt;
static const BigInt zero; // 仅声明
};
// 在源文件中初始化
const BigInt Accumulationtraits<BigInt>::zero = BigInt{0};
- 但这个方法仍有缺点,编译器不知道其他文件的定义,也就不知道zero的值为0。C++17中可以使用inline变量解决此问题
template<>
struct Accumulationtraits<BigInt> {
using AccT = BigInt;
inline static const BigInt zero = BigInt{0}; // OK since C++17
};
- C++17中的另一种优先做法是,为不总是产生整型的value traits使用内联成员函数,如果返回一个literal type,可以将函数声明为constexpr
template<typename T>
class Accumulationtraits;
template<>
struct Accumulationtraits<char> {
using AccT = int;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct Accumulationtraits<short> {
using AccT = int;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct Accumulationtraits<int> {
using AccT = long;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct Accumulationtraits<unsigned int> {
using AccT = unsigned long;
static constexpr AccT zero() {
return 0;
}
};
template<>
struct Accumulationtraits<float> {
using AccT = double;
static constexpr AccT zero() {
return 0;
}
};
- 接着扩展到自定义类型
template<>
struct Accumulationtraits<BigInt> {
using AccT = BigInt;
static BigInt zero() {
return BigInt{0};
}
};
- 使用的区别只是由访问静态数据成员改为使用函数调用语法
AccT total = Accumulationtraits<T>::zero();
1.3 Parameterized Traits
- 上面的traits称为fixed traits,因为一旦定义了这个traits就不能在算法中改写。如果要改写,可以通过添加一个模板实参AT给traits一个默认值来实现
template<typename T, typename AT = Accumulationtraits<T>>
auto accum (const T* beg, const T* end)
{
typename AT::AccT total = AT::zero();
while (beg != end) {
total += *beg;
++beg;
}
return total;
};
- 通常大多数使用这个模板的用户不用显式指定第二个模板实参,因为它可以由第一个实参类型配置合适的默认值
02 Traits versus Policies and Policy Classes
- 除了求和还有其他形式的累积问题,如求积、连接字符串,或者找出序列中的最大值。这些问题只需要修改total += *beg即可,这个算法就称为policy。policy类提供了算法的接口,下面是使用policy求积的例子
class MultPolicy {
public:
template<typename T1, typename T2>
static void accumulate (T1& total, const T2& value) {
total *= value;
}
};
template <typename T,
typename Policy = MultPolicy,
typename traits = Accumulationtraits<T>>
auto accum (const T* beg, const T* end)
{
using AccT = typename traits::AccT;
AccT total = traits::zero();
while (beg != end) {
Policy::accumulate(total, *beg);
++beg;
}
return total;
}
int main()
{
int num[] = { 1, 2, 3, 4, 5 };
std::cout << accum<int,MultPolicy>(num, num+5);
}
- 但输出结果却是0,原因是对于求积,0是错误的初值,这也说明了不同的traits和policy是互相影响的,实际上除了traits和policy还有其他解决方案,如标准库的std::accumulate()函数把初值作为第三个实参
2.1 Member Templates versus Template Template Parameters
- 之前把policy实现为含成员模板的普通类,下面用类模板实现policy类,并将其用作模板的模板参数来修改Accum接口
template <typename T1, typename T2>
class MultPolicy {
public:
static void accumulate (T1& total, const T2& value) {
total *= value;
}
};
template<typename T,
template<typename, typename> class Policy = MultPolicy,
typename traits = Accumulationtraits<T>>
auto accum (const T* beg, const T* end)
{
using AccT = typename traits::AccT;
AccT total = traits::zero();
while (beg != end) {
Policy<AccT, T>::accumulate(total, *beg);
++beg;
}
return total;
}
- 用类模板实现policy的优点是,可以让policy类携带一些依赖于模板参数的状态信息(即static数据成员),缺点则是使用时需要定义模板参数的确切个数,使得traits的表达式更冗长
2.2 traits和policy的区别
- traits更注重于类型
- traits可以不需要通过额外的模板参数来传递(fixed traits)
- traits参数通常有十分自然的、很少或不能被改写的默认值
- traits参数紧密依赖于一个或多个主参数
- traits一般包含类型和常量
- traits通常由traits模板实现
- policy更注重于行为
- policy calss需要额外的模板参数来传递
- policy参数不需要有默认值,通常都是显式指定(尽管许多泛型组件都配置了使用频率很高的缺省policy)
- policy参数和模板的其他模板参数通常是正交的
- policy类一般包含成员函数
- policy既可以用普通类实现,也可以用类模板实现
- traits和policy可以控制模板参数个数,但是多个traits和policy的排序问题就出现了。一种简单的策略是按使用频率升序排序,即traits参数位于policy参数右边,因为policy参数通常会被改写
2.3 运用普通的迭代器进行累积
- 下面是一个新版本的的accum,它不仅支持指针,还支持普通的迭代器
#include <iterator>
template<typename Iter>
auto accum (Iter start, Iter end)
{
using VT = typename std::iterator_traits<Iter>::value_type;
VT total{};
while (start != end) {
total += *start;
++start;
}
return total;
}
- std::iterator_traits封装了迭代器的相关属性
namespace std {
template<typename T>
struct iterator_traits<T*> {
using difference_type = ptrdiff_t;
using value_type = T;
using pointer = T*;
using reference = T&;
using iterator_category = random_access_iterator_tag ;
};
}
03 类型函数(Type Functions)
- 通常所说的函数指值函数,参数和返回结果都是值。类型函数接受类型实参,并生成一个类型作为返回值。sizeof就是一个类型函数,它返回一个给定类型的实参大小的常量。类模板也可以作为类型函数,如把sizeof改写如下
#include <iostream>
template<typename T>
class TypeSize {
public:
static const std::size_t value = sizeof(T);
};
int main()
{
std::cout << TypeSize<int>::value; // 4
}
3.1 确定元素类型
- 假如有如std::vector<T>、std::list<T>的容器模板,要对给定的容器给出元素的类型,下面用局部特化实现
#include <iostream>
#include <vector>
#include <list>
template<typename T>
struct ElementT; // primary template
template<typename T>
struct ElementT<std::vector<T>> { //partial specialization for std::vector
using Type = T;
};
template<typename T>
struct ElementT<std::list<T>> { // partial specialization for std::list
using Type = T;
};
template<typename T, std::size_t N>
struct ElementT<T[N]> { // partial specialization for arrays of known bounds
using Type = T;
};
template<typename T>
struct ElementT<T[]> { // partial specialization for arrays of unknown bounds
using Type = T;
};
template<typename T>
void printElementType (const T& c)
{
std::cout << typeid(typename ElementT<T>::Type).name();
}
int main()
{
std::vector<bool> s;
printElementType(s); // bool
int arr[42];
printElementType(arr); // int
}
- 局部特化在容器类型不用知道类型函数的情况下实现类型萃取。但大多数情况下,类型函数是和容器类型一起实现的,实现能被简化,如果容器类型定义了一个成员类型value_type,可以编码如下
template<typename C>
struct ElementT {
using Type = typename C::value_type;
};
- 上面可以作为一种缺省实现,而对于没有定义value_type的容器则可以进行特化。容器中通常会定义模板参数类型以便于访问
template<typename T1, typename T2, ...>
class X {
public:
using ... = T1;
using ... = T2;
...
};
- 类型函数允许根据容器类型来参数化模板,而不需要指定元素类型的模板参数,比如
template<typename T, typename C>
T sumOfElements (const C& c);
- 可以更方便地写为
template<typename C>
typename ElementT<C>::Type sumOfElements (const C& c);
- 为了方便,可以为类型函数引入别名模板以简化代码
template<typename T>
using ElementType = typename ElementT<T>::Type;
template<typename C>
ElementType<C> sumOfElements (const C& c);
3.2 Transformation traits
3.2.1 移除引用
template<typename T>
struct RemoveReferenceT {
using Type = T;
};
template<typename T>
struct RemoveReferenceT<T&> {
using Type = T;
};
template<typename T>
struct RemoveReferenceT<T&&> {
using Type = T;
};
template<typename T>
using RemoveReference = typename RemoveReference<T>::Type;
- 标准库提供了对应的std::remove_reference
3.2.2 添加引用
template<typename T>
struct AddLValueReferenceT {
using Type = T&;
};
template<typename T>
using AddLValueReference = typename AddLValueReferenceT<T>::Type;
template<typename T>
struct AddRValueReferenceT {
using Type = T&&;
};
template<typename T>
using AddRValueReference = typename AddRValueReferenceT<T>::Type;
- 引用折叠在这里会生效,如AddLValueReference<int&&>将折叠为int&
- 如果让AddLValueReferenceT和AddRValueReferenceT保持原样而不引入特化,可以将其简化为
template<typename T>
using AddLValueReferenceT = T&;
template<typename T>
using AddRValueReferenceT = T&&;
- 这样实例化时可以不用实例化类模板,但想为一些特殊情况特化这些模板时是有风险的,比如不能用void作为模板实参,为此需要对void特化
template<>
struct AddLValueReferenceT<void> {
using Type = void;
};
template<>
struct AddLValueReferenceT<void const> {
using Type = void const;
};
template<>
struct AddLValueReferenceT<void volatile> {
using Type = void volatile;
};
template<>
struct AddLValueReferenceT<void const volatile> {
using Type = void const volatile;
};
- 因为别名模板不能被特化,所以别名模板必须根据类模板来构建,以确保特化也能被picked up
- 标准库提供了对应的std::add_lvalue_reference和std::add_rvalue_reference,标准模板包含了void类型的特化
3.2.3 移除限定符
template<typename T>
struct RemoveConstT {
using Type = T;
};
template<typename T>
struct RemoveConstT<T const> {
using Type = T;
};
template<typename T>
using RemoveConst = typename RemoveConstT<T>::Type;
- 此外,transformation traits能被组合,比如创建一个RemoveCVT traits,能同时移除const和volatile
template<typename T>
struct RemoveCVT : RemoveConstT<typename RemoveVolatileT<T>::Type> {
};
template<typename T>
using RemoveCV = typename RemoveCVT<T>::Type;
- 关于RemoveCVT的定义,有两点要注意。一是它同时使用了RemoveConstT和RemoveVolatileT,会首先移除volatile(如果存在),随后把结果类型传给RemoveConstT。二是它使用元函数转发(metafunction forwarding)从RemoveConstT继承类型成员,而不是自己声明一个和RemoveConstT特化相同的类型成员。这里元函数转发只是简单地用来减少RemoveCVT定义的代码量,但当元函数没有为所有输入定义时,元函数转发也是有用的
- 模板RemoveCV可以用别名简化如下,只有RemoveCVT没有特化时才能这样
template<typename T>
using RemoveCV = RemoveConst<RemoveVolatile<T>>;
3.2.4 Decay
- 模仿传值时的类型转换,即把数组和函数转指针,并去除顶层cv或引用
#include <iostream>
#include <type_traits>
template<typename T>
void f(T) {}
template<typename A>
void printParameterType(void (*)(A))
{
std::cout << "Parameter type: " << typeid(A).name() << '\n';
std::cout << "- is int: " << std::is_same<A, int>::value << '\n';
std::cout << "- is const: " << std::is_const<A>::value << '\n';
std::cout << "- is pointer: " << std::is_pointer<A>::value << '\n';
}
int main()
{
printParameterType(&f<int>); // 未改变
printParameterType(&f<int const>); // 衰退为int
printParameterType(&f<int[7]>); // 衰退为int*
printParameterType(&f<int(int)>); // 衰退为int(*)(int)
}
- 可以实现一个传值时产生相同的类型转换的traits
#include <iostream>
#include <type_traits>
// 首先定义nonarray,nonfunction的情况,移除任何cv限定符
template<typename T>
struct DecayT : RemoveCVT<T> {
};
// 使用局部特化处理数组到指针的decay,要求识别任何数组类型
template<typename T>
struct DecayT<T[]> {
using Type = T*;
};
template<typename T, std::size_t N>
struct DecayT<T[N]> {
using Type = T*;
};
// 函数到指针的decay,必须匹配任何函数类型
template<typename R, typename... Args>
struct DecayT<R(Args...)> {
using Type = R (*)(Args...);
};
template<typename R, typename... Args>
struct DecayT<R(Args..., ...)> { // 匹配使用C-style vararg的函数类型
using Type = R (*)(Args..., ...);
};
template<typename T>
using Decay = typename DecayT<T>::Type;
template<typename T>
void printDecayedType()
{
using A = Decay<T>;
std::cout << "Parameter type: " << typeid(A).name() << '\n';
std::cout << "- is int: " << std::is_same<A, int>::value << '\n';
std::cout << "- is const: " << std::is_const<A>::value << '\n';
std::cout << "- is pointer: " << std::is_pointer<A>::value << '\n';
}
int main()
{
printDecayedType<int>();
printDecayedType<int const>();
printDecayedType<int[7]>();
printDecayedType<int(int)>();
}
- 标准库提供了对应的std::decay
3.2.5 C++98中的处理
#include <iostream>
template<typename T>
void apply (T& arg, void (*func)(T))
{
func(arg);
}
void print (int a) { std::cout << a << std::endl; }
void incr (int& a) { ++a; }
int main()
{
int x = 7;
apply (x, print); // 1,OK
apply (x, incr); // 2,error
}
- 1处int替换T,则apply的参数类型分别是int&和void(*)(int),而2处要用int&替换T才能匹配,导致第一个参数类型不匹配而出错。解决方法是创建一个类型函数,给定类型本身不是引用则使用引用运算符。另外也可以提供对应的去除引用运算符的操作,前提是类型本身是一个引用。同理也可以添加或去除const限定符,这些可以用局部特化实现
template <typename T>
class TypeOp { // primary template
public:
typedef T ArgT;
typedef T BareT;
typedef T const ConstT;
typedef T & RefT;
typedef T & RefBareT;
typedef T const & RefConstT;
};
template <typename T>
class TypeOp <T const> { // partial specialization for const types
public:
typedef T const ArgT;
typedef T BareT;
typedef T const ConstT;
typedef T const & RefT;
typedef T & RefBareT;
typedef T const & RefConstT;
};
template <typename T>
class TypeOp <T&> { // partial specialization for references
public:
typedef T & ArgT;
typedef typename TypeOp<T>::BareT BareT;
typedef T const ConstT;
typedef T & RefT;
typedef typename TypeOp<T>::BareT & RefBareT;
typedef T const & RefConstT;
};
// 不允许指向void的引用,可以将其看成普通的void类型
template<>
class TypeOp <void> { // full specialization for void
public:
typedef void ArgT;
typedef void BareT;
typedef void const ConstT;
typedef void RefT;
typedef void RefBareT;
typedef void RefConstT;
};
template <typename T>
void apply (typename TypeOp<T>::RefT arg, void (*func)(T))
{
func(arg);
}
- 注意T位于受限名称中,不能被第一个实参推断出来,因此只能根据第二个实参推断T,再根据结果生成第一个参数的实际类型
3.3 Promotion Traits(第一版内容)
- 上述提到的都是单一类型的类型函数,即给定一个类型可以定义其他相关类型或参数。但通常需要依赖于多个实参的类型函数,这就是promotion traits,它在编写运算符模板时很有用,下面先编写一个用于两个Array容器相加的函数模板
template<typename T>
Array<T> operator+ (Array<T> const&, Array<T> const&);
- 隐式转换允许char与int相加,因此希望对数组实现混合类型的操作,问题在于如何确定返回类型
template<typename T1, typename T2>
Array<???> operator+ (Array<T1> const&, Array<T2> const&);
- promotion traits可以解决上面声明中的问题
template<typename T1, typename T2>
Array<typename Promotion<T1, T2>::ResultT>
operator+ (Array<T1> const&, Array<T2> const&);
// 或使用另一种实现
template<typename T1, typename T2>
typename Promotion<Array<T1>, Array<T2> >::ResultT
operator+ (Array<T1> const&, Array<T2> const&);
- 参数为两个不同类型时,通常希望返回两者中更强大的类型,这时就会用到类型函数。promotion模板实际上不存在确切定义,因此最好让这个基本模板处于未定义状态
template<typename T1, typename T2>
class Promotion;
- 如果两个类型大小不一样,则要提升更强大的类型,通过模板IfThenElse实现这点,它接受一个Boolean的非类型模板参数来选择两个类型参数中的一个
// traits/ifthenelse.hpp
#ifndef IFTHENELSE_HPP
#define IFTHENELSE_HPP
// primary template: yield second or third argument depending on first argument
template<bool C, typename Ta, typename Tb>
class IfThenElse;
// partial specialization: true yields second argument
template<typename Ta, typename Tb>
class IfThenElse<true, Ta, Tb> {
public:
typedef Ta ResultT;
};
// partial specialization: false yields third argument
template<typename Ta, typename Tb>
class IfThenElse<false, Ta, Tb> {
public:
typedef Tb ResultT;
};
#endif // IFTHENELSE_HPP
- 有了上述代码就可以实现如下promotion模板,根据需要提升的类型大小在T1,T2,void三者中选择
// traits/promote1.hpp
// primary template for type promotion
template<typename T1, typename T2>
class Promotion {
public:
typedef typename
IfThenElse<(sizeof(T1)>sizeof(T2)),
T1,
typename IfThenElse<(sizeof(T1)<sizeof(T2)),
T2,
void
>::ResultT
>::ResultT ResultT;
};
- 在基本模板中使用这种基于类型大小的探索大多数情况下都可以正常运行,但需要检验。如果选择了错误的类型,必须再写一个相应的特化来改写这种选择。另一方面,如果两种类型完全一样,就可以安全地提升类型,下面的局部特化说明了这一点
// traits/promote2.hpp
// partial specialization for two identical types
template<typename T>
class Promotion<T,T> {
public:
typedef T ResultT;
};
- 为了记录基本类型的提升,需要实现一系列特化,借助宏可以减少代码量
// traits/promote3.hpp
#define MK_PROMOTION(T1,T2,Tr) \
template<> class Promotion<T1, T2> { \
public: \
typedef Tr ResultT; \
}; \
\
template<> class Promotion<T2, T1> { \
public: \
typedef Tr ResultT; \
};
- 可以这样添加这些提升
// traits/promote4.hpp
MK_PROMOTION(bool, char, int)
MK_PROMOTION(bool, unsigned char, int)
MK_PROMOTION(bool, signed char, int)
...
- 这是一个比较直接的方法,但需要枚举出几十种可能的基本类型组合。事实上还存在许多可用的技术,如可以修改IsFundaT和IsEnumT模板定义基于整型和浮点型的提升类型,promotion只需要对这些基本类型进行特化,一旦为基本类型定义好了promotion就可以通过局部特化表达其他提升规则,对于Array数组如下
// traits/promotearray.hpp
template<typename T1, typename T2>
class Promotion<Array<T1>, Array<T2> > {
public:
typedef Array<typename Promotion<T1,T2>::ResultT> ResultT;
};
template<typename T>
class Promotion<Array<T>, Array<T> > {
public:
typedef Array<typename Promotion<T,T>::ResultT> ResultT;
};
- 注意最后一个局部特化,Promotion<Array<T1>, Array<T2>>和Promotion<T, T>特化程度是一样的,为了避免模板选择的二义性,添加了最后一个更加特殊的局部特化
3.4 Predicate Traits
3.4.1 IsSameT
- IsSameT traits判断两个类型是否相等
template<typename T1, typename T2>
struct IsSameT {
static constexpr bool value = false;
};
template<typename T>
struct IsSameT<T, T> {
static constexpr bool value = true;
};
- 检查模板参数是否为整型
if (IsSameT<T, int>::value) ...
- 对于产生常量值的traits,不能提供别名模板,但可以使用constexpr变量模板替代
template<typename T1, typename T2>
constexpr bool isSame = IsSameT<T1, T2>::value;
- 标准库提供了对应的std::is_same
3.4.2 true_type和false_type
- 可以为两种可能的输出提供不同类型来改进IsSameT的定义,如果声明一个类模板BoolConstant,它带有两种可能的实例化TrueType和FalseType,则可以让IsSameT派生自TrueType或FalseType,这个技术称为tag dispatching
#include <iostream>
template<bool val>
struct BoolConstant {
using Type = BoolConstant<val>;
static constexpr bool value = val;
};
using TrueType = BoolConstant<true>;
using FalseType = BoolConstant<false>;
template<typename T1, typename T2>
struct IsSameT : FalseType
{};
template<typename T>
struct IsSameT<T, T> : TrueType
{};
template<typename T>
void fooImpl(T, TrueType)
{
std::cout << "fooImpl(T,true) for int called\n";
}
template<typename T>
void fooImpl(T, FalseType)
{
std::cout << "fooImpl(T,false) for other type called\n";
}
template<typename T>
void foo(T t)
{
fooImpl(t, IsSameT<T,int>{}); // 根据T是否为int选择实现
}
int main()
{
foo(42); // calls fooImpl(42, TrueType)
foo(7.7); // calls fooImpl(42, FalseType)
}
- BoolConstant实现包含一个Type成员,这允许使用别名模板来简化
template<typename T>
using IsSame = typename IsSameT<T>::Type;
- 为了支持泛型,应该分别只有一个类型代表true和false,C++11在<type_traits>中提供了std::true_type和std::false_type
// C++11/14中的定义
namespace std {
using true_type = integral_constant<bool, true>;
using false_type = integral_constant<bool, false>;
}
// C++17中的定义
namespace std {
using true_type = bool_constant<true>;
using false_type = bool_constant<false>;
}
// 其中bool_constant的定义
template<bool B>
using bool_constant = integral_constant<bool, B>;
3.5 Result Type traits
- result type traits是另一个处理多种类型的类型函数,常用于操作符模板,如写一个对两个数组相加的模板声明时,返回类型的确定
template<typename T1, typename T2>
Array<typename PlusResultT<T1, T2>::Type>
operator+ (Array<T1> const&, Array<T2> const&);
- 假设别名模板有效,可以写为
template<typename T1, typename T2>
Array<PlusResult<T1, T2>>
operator+ (Array<T1> const&, Array<T2> const&);
- PlusResultT traits决定两个可能类型不同的值相加后类型
// traits/plus1.hpp
template<typename T1, typename T2>
struct PlusResultT {
using Type = decltype(T1() + T2());
};
template<typename T1, typename T2>
using PlusResult = typename PlusResultT<T1, T2>::Type;
- 这个traits模板使用decltype推断类型,但实际上decltype包含了太多信息,比如PlusResultT的构建可能产生一个引用类型,但是也许这里的数组类模板的设计没有对引用类型的处理,更实际的,重载的operator+还可能返回一个const class类型的值
class Integer { ... };
Integer const operator+ (Integer const&, Integer const&);
- 这样需要做的就是移除引用限定符来转换结果类型
template<typename T1, typename T2>
Array<RemoveCV<RemoveReference<PlusResult<T1, T2>>>>
operator+ (Array<T1> const&, Array<T2> const&);
- 这样的嵌套traits在模板库和元编程中是很常用的
- 然而还有一个问题是,表达式T1()+T2()会尝试值初始化,需要元素类型为T1和T2的默认构造函数,但数组类本身可能不要求元素类型的值初始化
3.5.1 declval
- 标准库提供了std::declval来产生值但不要求构造函数,它定义在<utility>中
namespace std {
template<typename T>
add_rvalue_reference_t<T> declval() noexcept;
}
- 这个函数模板是特意未定义的,因为它只针对于在decltype、sizeof或其他不需要定义的上下文中使用。它有两个有趣的属性:
- 对于引用类型,返回类型总是该类型的右值引用,这使得declval甚至能处理不能从函数正常返回的类型,比如抽象类类型或数组类型。declval<T>()用作表达式时,从T到T&&的转换由于引用折叠对declval<T>()的行为没有影响
- declval本身不会抛出异常,在noexcept运算符的语境中使用时很有用
#include <utility>
template<typename T1, typename T2>
struct PlusResultT {
using Type = decltype(std::declval<T1>() + std::declval<T2>());
};
template<typename T1, typename T2>
using PlusResult = typename PlusResultT<T1, T2>::Type;
04 SFINAE-based traits
4.1 SFINAE Out函数重载
- 对函数重载使用SFINAE找出一个类型是否默认可构造,由此可以创建一个不需要任何值初始化的对象,即对于T,表达式T()必须有效
#include "issame.hpp"
template<typename T>
struct IsDefaultConstructibleT {
private:
// test() trying substitute call of a default constructor for T passed as U :
template<typename U, typename = decltype(U())>
static char test(void*);
// test() fallback:
template<typename>
static long test(...);
public:
static constexpr bool value
= IsSameT<decltype(test<T>(nullptr)), char>::value;
};
- 通常对函数重载实现基于SFINAE traits的写法就是声明两个不同返回类型的重载函数模板,第一个只在需要的检查成功时匹配,第二个是fallback:可变参数总能匹配任何调用,但如果更好的匹配就会选择其他的
template<...> static char test(void*);
template<...> static long test(...);
- 返回值取决于哪个重载的test成员被选择,选择返回类型为char的第一个,值就被初始化为isSame<char, char>,结果为true,选择第二个则为isSame<long, char>,结果为false
static constexpr bool value
= IsSameT<decltype(test<...>(nullptr)), char>::value;
- 现在来判断T()能否默认构造,首先把T作为U传递,并给出第一个test()声明一个仿制的未命名模板实参,它带有一个仅当转换有效时才可行的构造。在这个例子中,使用的这个表达式仅当U()存在才有效
- 没有对应的实参传递,第二个模板参数不能被推断,因此它将被替代,如果替代失败,由于SFINAE,test()声明将被丢弃,只有fallback声明将匹配,因此可以如下使用这个traits
IsDefaultConstructibleT<int>::value // yields true
struct S {
S() = delete;
};
IsDefaultConstructibleT<S>::value // yields false
- 注意不能在第一个test()中直接使用模板参数T,因为对任何T,所有成员函数都将被替换,因此对于不能默认构造的类型,编译将失败而不是忽略第一个test()重载。通过传递T给U,只对第二个test()重载创建了一个具体的SFINAE上下文
template<typename T>
struct IsDefaultConstructibleT {
private:
// ERROR: test() uses T directly:
template<typename, typename = decltype(T())>
static char test(void*);
// test() fallback:
template<typename>
static long test(...);
public:
static constexpr bool value
= IsSameT<decltype(test<T>(nullptr)), char>::value;
};
4.1.1 其他可选的SFINAE-based traits实现策略
- SFINAE-based traits从C++98开始就可以实现了,关键就是声明两个返回类型不同的重载函数模板
template<...> static char test(void*);
template<...> static long test(...);
- 然而最初的技术使用返回类型的大小来确定哪个重载被选择(仍使用0和enum,因为nullptr和constexpr还不可用)
enum { value = sizeof(test<...>(0)) == 1 };
- 一些平台上可能发生sizeof(char) == sizeof(long),比如在DSP或老的Cray机器上所有的整型都有同样的大小,如果sizeof(char)定义为1,sizeof(long)甚至sizeof(long long)都等于1。要确定在所有平台上有不同的大小,比如定义
using Size1T = char;
using Size2T = struct { char a[2]; };
// 或者
using Size1T = char(&)[1];
using Size2T = char(&)[2];
- 可以定义test()重载如下
template<...> static Size1T test(void*); // checking test()
template<...> static Size2T test(...); // fallback
- 这里要么返回一个Size1T,只有单个size1的char,要么返回两个char大小的,在所有平台不小于size2的数组
- 任何类型都可以传递给func(),所有实参都可以匹配期望的类型,如甚至可以传递整型数42
template<...> static Size1T test(int); // checking test()
template<...> static Size2T test(...); // fallback
...
enum { value = sizeof(test<...>(42)) == 1 };
4.1.2 Making SFINAE-based traits Predicate traits
- 断言traits返回一个派生自std::true_type或std::false_type的布尔值,这个方法也可以解决一些平台上sizeof(char)==sizeof(long)的问题。首先需要一个IsDefaultConstructibleT的直接定义,这个traits应该派生自辅助类的Type,可以简单提供对应的基类作为test()重载的返回类型
template<...> static std::true_type test(void*); // checking test()
template<...> static std::false_type test(...); // fallback
- 基类的Type成员可以简单地声明如下,并且不再需要IsSameT traits
using Type = decltype(test<FROM>(nullptr));
- IsDefaultConstructibleT改进后的完整实现如下
#include <type_traits>
template<typename T>
struct IsDefaultConstructibleHelper {
private:
// test() trying substitute call of a default constructor for T passed as U:
template<typename U, typename = decltype(U())>
static std::true_type test(void*);
// test() fallback:
template<typename>
static std::false_type test(...);
public:
using Type = decltype(test<T>(nullptr));
};
template<typename T>
struct IsDefaultConstructibleT :
IsDefaultConstructibleHelper<T>::Type {
};
- 如果第一个test()函数有效,就是更好的重载,成员IsDefaultConstructibleHelper::Type由它的返回类型std::true_type初始化,因此IsConvertibleT<...>派生自std::true_type。如果第一个test()无效,则由第二个test()的返回类型初始化,因此IsConvertibleT<...>派生自std::false_type
4.2 SFINAE Out局部特化
- 第二个实现SFINAE-based traits的方法是使用局部特化,下面同样是一个确定T是否默认可构造的例子
#include "issame.hpp"
#include <type_traits> // defines true_type and false_type
// helper to ignore any number of template parameters:
template<typename...> using VoidT = void;
// primary template:
template<typename, typename = VoidT<>>
struct IsDefaultConstructibleT : std::false_type
{
};
// partial specialization (may be SFINAE'd away):
template<typename T>
struct IsDefaultConstructibleT<T, VoidT<decltype(T())>> : std::true_type
{
};
- 和上面的predicate traits一样,为改进的IsDefaultConstructibleT定义了一个派生自std::false_type的通用情况,因为一个类型默认没有成员size_type
- 这里一个有趣的特性是,第二个模板实参默认为一个辅助的VoidT类型,它允许提供使用任意数量编译期类型构造的局部特化。这个例子中,只需要一个decltype(T())构造来检查T的默认构造函数是否有效,如果无效则SFINAE使得整个局部特化被丢弃,fall back到primary template,否则局部特化有效并被优先选用
- C++17中,标准库引入了一个type traits std::void_t<>,对应于这里引入的VoidT,C++17前可以自行定义如上,或甚至可以定义在std中
#include <type_traits>
#ifndef __cpp_lib_void_t
namespace std {
template<typename...> using void_t = void;
}
#endif
- 比起重载函数模板,局部特化定义type traits明显看起来更精炼,但是它需要在模板参数声明中构建条件的能力。使用带函数重载的类模板允许使用附加的辅助函数或者辅助类型
4.3 为SFINAE使用泛型lambda
- 无论使用哪种技术,总需要一些公式化的代码定义traits:重载和调用两个test()成员函数或实现多个局部特化。C++17中可以最简化公式化的代码,方法是在一个泛型lambda中指定检查条件
- 首先引入一个由两个嵌套泛型lambda表达式构造的工具
// traits/isvalid.hpp
#include <utility>
// helper: checking validity of f (args...) for F f and Args... args:
template<typename F, typename... Args,
typename = decltype(std::declval<F>() (std::declval<Args&&>()...))>
std::true_type isValidImpl(void*);
// fallback if helper SFINAE'd out:
template<typename F, typename... Args>
std::false_type isValidImpl(...);
// define a lambda that takes a lambda f and returns whether
// calling f with args is valid
inline constexpr
auto isValid = [](auto f) {
return [](auto&&... args) {
return
decltype(isValidImpl<decltype(f), decltype(args)&&...>(nullptr)) {};
};
};
// helper template to represent a type as a value
template<typename T>
struct TypeT {
using Type = T;
};
// helper to wrap a type as a value
template<typename T>
constexpr auto type = TypeT<T>{};
// helper to unwrap a wrapped type in unevaluated contexts
template<typename T>
T valueT(TypeT<T>); // no definition needed
- isValid是一个constexpr变量,为lambda闭包类型,声明必须用auto,因为C++没有直接表示闭包类型的方法。C++17前lambda表达式不能出现在常量表达式中,所以这个代码只在C++17中有效
- 在研究内部的lambda之前,先对isValid检查一个典型的使用
constexpr auto isDefaultConstructible
= isValid([](auto x) -> decltype((void)decltype(valueT(x))() {});
- isDefaultConstructible有一个lambda闭包类型,它是一个用来检查类型能否被默认构造的traits的函数对象,换句话说,isValid是一个traits factory:一个产生从它的实参检查对象的traits的组件
- type辅助变量模板允许把一个类型表达为一个值,一个用它包含的值x可以使用decltype(valueT(x))转回原始类型,这就是传给isValid的lambda所做的。如果萃取的类型不能被默认构造,decltype(valueT(x))()是无效的,我们将得到一个编译器报错或一个被SFINAE out的关联声明(这就是要借由isValid的定义实现的)。isDefaultConstructible可以使用如下
isDefaultConstructible(type<int>) // true(int是默认可构造的)
isDefaultConstructible(type<int&>) // false(引用不是默认可构造的)
- 为了明白每个细节如何一起工作,考虑当isValid的参数f绑定到isDefaultConstructible定义中的泛型lambda实参时,isValid内部的lambda表达式将变成什么。通过执行isValid定义中的替换,得到等价的
constexpr auto isDefaultConstructible
= [](auto&&... args) {
return decltype(
isValidImpl<
decltype([](auto x)
->
decltype((void)decltype(valueT(x))())),
decltype(args)&&...
>(nullptr)){};
};
- 回头看上面的isValidImpl()的第一个声明,注意到它包含一个默认模板实参
decltype(std::declval<F>()(std::declval<Args&&>()...))>
- 它尝试调用一个它首个模板实参类型的值,值类型是isDefaultConstructible定义中的lambda的闭包类型,带有传递给isDefaultConstructible的实参(decltype(args)&&...)类型的值。当只有一个参数x在lambda中时,args必须扩展为仅一个实参
- 在之前的static_assert的例子中,实参有类型类型TypeT<int>或TypeT<int&>。在TypeT<int&>中,decltype(valueT(x))是int&,这将使得decltype(valueT(x))()无效,因此第一个isValidImpl()声明中的默认模板实参将失败,并被SFINAE out。这样只剩下产生一个false_type值的第二个声明,即传递type<int&>时isDefaultConstructibel产生false_type。如果传递type<int>,替换不会失效,第一个isValidImpl()的声明被选中,产生一个true_type值
- 要让SFINAE生效,替换必须发生在被替换模板的immediate context中。这里被替换的模板是第一个isvalidImpl的声明和传递给isValid的泛型lambda调用符,因此被测试的构造必须出现在lambda的返回类型中而非函数体中
- isDefaultConstructible traits和之前需要用函数风格调用替代具体的模板实参的traits实现有一些不同,这可以说是一种更可读的记号,但之前的风格也可以被得到
template<typename T>
using IsDefaultConstructibleT
= decltype(isDefaultConstructible(std::declval<T>()));
- 这时一个传统的模板声明,然而它只能出现在namespace scope中,而isDefaulConstructible的定义可以在block scope中被引入
- 至今这个技术可能还不能通过编译,因为被调用的表达式和使用风格十分复杂。然而一旦isValid可行,许多traits都能只用一个声明实现。比如对一个名为first成员的访问测试十分简洁
constexpr auto hasFirst
= isValid([](auto x) -> decltype((void)valueT(x).first) {});
4.4 SFINAE-Friendly traits
- 通常,一个type traits应该可以回应一个特殊的查询而不造成程序非法。SFINAE-based traits困住了隐藏的问题,把错误变成了否定的结果。然而,一些traits在面对错误时就不能表现好了,比如之前的PlusResultT
#include <utility>
template<typename T1, typename T2>
struct PlusResultT {
using Type = decltype(std::declval<T1>() + std::declval<T2>());
};
template<typename T1, typename T2>
using PlusResult = typename PlusResultT<T1, T2>::Type;
- 在这个定义中,+被使用于未被SFINAE保护的context中,因此如果程序尝试对没有合适的operator+的类型估计PlusResultT则会出错,就像下面尝试声明不相关的A和B类型的数组之和的返回类型
template<typename T>
class Array {
...
};
// declare + for arrays of different element types:
template<typename T1, typename T2>
Array<typename PlusResultT<T1, T2>::Type>
operator+ (Array<T1> const&, Array<T2> const&);
- 明显地,如果数组元素没有定义对应的operator,这里使用PlusResultT<>将造成错误
class A {
};
class B {
};
void addAB(Array<A> arrayA, Array<B> arrayB) {
auto sum = arrayA + arrayB; // ERROR:初始化PlusResultT<A, B>失败
...
}
- 实际出现的问题不像这样明显(无法相加A和B的数组),通常发生于operator+的模板实参推断过程中,隐藏在PlusResultT<A, B>的实例化中。这有一个引人注目的结果:即使添加一个特定的重载用来相加A和B数组,程序也可能无法编译,因为C++不能确定如果另一个重载更好时,函数模板中的类型能否实例化
// declare generic + for arrays of different element types:
template<typename T1, typename T2>
Array<typename PlusResultT<T1, T2>::Type>
operator+ (Array<T1> const&, Array<T2> const&);
// overload + for concrete types:
Array<A> operator+(Array<A> const& arrayA, Array<B> const&
arrayB);
void addAB(Array<A> const& arrayA, Array<B> const& arrayB) {
auto sum = arrayA + arrayB; // ERROR?取决于编译器是否实例化PlusResultT<A,B>
...
}
- 如果编译器能确定第二个operator+的声明是更好的匹配,则可以通过。推断和替换候选函数模板时,类模板定义的实例化期间发生的任何事都不是函数模板替换的immediate context的一部分,于是在PlusResultT<>中对A和B类型的元素调用operator+就会出错
template<typename T1, typename T2>
struct PlusResultT {
using Type = decltype(std::declval<T1>() + std::declval<T2>());
};
- 为了解决这个问题,必须使PlusResultT SFINAE-friendly,即通过给它一个合适的定义使其更有弹性,即使它的decltype表达式是非法的
- 定义一个HasPlusT traits来检查给定的类型是否有合适的operator+
// traits/hasplus.hpp
#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and void_t
// primary template:
template<typename, typename, typename = std::void_t<>>
struct HasPlusT : std::false_type
{ };
// partial specialization (may be SFINAE'd away):
template<typename T1, typename T2>
struct HasPlusT<T1, T2,
std::void_t<decltype(std::declval<T1>()+ std::declval<T2>())>
>
: std::true_type
{ };
- 如果它产生true,PlusResultT能使用已有实现,否则PlusResultT需要一个safe default,对于一个没有有意义的结果traits最好的default就是不提供任何成员Type,这样如果traits用在SFINAE context(如上面的数组operator+模板的返回类型)中,丢失的成员Type将使模板实参推断失败,这正是数组operator+模板想要的结果。下面的实现提供了这个行为
#include "hasplus.hpp"
template<typename T1, typename T2, bool = HasPlusT<T1, T2>::value>
struct PlusResultT { // primary template, used when HasPlusT yields true
using Type = decltype(std::declval<T1>() + std::declval<T2>());
};
template<typename T1, typename T2>
struct PlusResultT<T1, T2, false> { // partial specialization, used otherwise
};
- 再次考虑Array<A>和Array<B>的相加,在上面这个实现中,PlusResultT<A, B>的实例化将不会有Type成员,因此operator+模板无效,SFINAE将从考虑中除去函数模板,针对Array<A>和Array<B>重载的operator+将被选择
- 作为通用的设计原则,如果给出合理的模板实参作为输入,一个traits模板应该永远不会在实例化期间失败。通用的方法是执行两次对应的检查
- 一次确定操作是否有效
- 一次计算结果
05 IsConvertibleT
细节很重要,因此对SFINAE-based traits通用的方法在实践中可能变得更复杂。为了阐述这点,定义一个traits,它可以确定一个给定的类型能否转换为另一个给定的类型——比如如果我们期望一个确定的基类或一个它的派生类。IsConvertibleT traits判断是否能把第一个传递的类型转换为第二个
#include <type_traits> // for true_type and false_type
#include <utility> // for declval
template<typename FROM, typename TO>
struct IsConvertibleHelper {
private:
// test() trying to call the helper aux(TO) for a FROM passed as F :
static void aux(TO);
template<typename F, typename T,
typename = decltype(aux(std::declval<F>()))>
static std::true_type test(void*);
// test() fallback:
template<typename, typename>
static std::false_type test(...);
public:
using Type = decltype(test<FROM>(nullptr));
};
template<typename FROM, typename TO>
struct IsConvertibleT : IsConvertibleHelper<FROM, TO>::Type {
};
template<typename FROM, typename TO>
using IsConvertible = typename IsConvertibleT<FROM, TO>::Type;
template<typename FROM, typename TO>
constexpr bool isConvertible = IsConvertibleT<FROM, TO>::value;
- 这里使用了之前的函数重载的方法,在辅助类中声明两个返回不同类型的名为test()的重载函数模板,以及一个Type成员
template<...> static std::true_type test(void*);
template<...> static std::false_type test(...);
...
using Type = decltype(test<FROM>(nullptr));
...
template<typename FROM, typename TO>
struct IsConvertibleT : IsConvertibleHelper<FROM, TO>::Type {};
- 和之前一样,第一个test只匹配检查成功的情况,第二个是fallback,因此目标是使第一个test当且仅当类型FROM转换为TO时有效。为了实现这点,仅当转换有效时,给第一个test声明一个匿名的构造初始化有效的模板实参。模板参数不能被推断,无法给它显式提供模板实参,因此它将被替换,替换失效则test声明被丢弃
- 再次注意,下面这样不可行
static void aux(TO);
template<typename = decltype(aux(std::declval<FROM>()))>
static char test(void*);
- 这里当成员函数模板被解析,FROM和TO是完全确定的,因此一对类型的转换是无效的(比如double和int),在调用test()前将报错,因此引入了F作为一个特定的成员函数模板参数
static void aux(TO);
template<typename F, typename = decltype(aux(std::declval<F>()))>
static char test(void*);
- 并在test()的调用中提供FROM类型作为一个显式模板实参
static constexpr bool value
= isSame<decltype(test<FROM>(nullptr)), char>;
- 注意std::declval不调用任何构造函数产生值。如果值可以转换为TO,aux()的调用是有效的,这个test()的声明匹配,否则发生SFINAE错误,fallback声明将匹配
- 作为结果可以使用traits如下
IsConvertibleT<int, int>::value // yields true
IsConvertibleT<int, std::string>::value // yields false
IsConvertibleT<char const*, std::string>::value // yields true
IsConvertibleT<std::string, char const*>::value // yields false
- 有三种情况不能被IsConvertibleT正确处理
- 转换为数组类型应该总是产生false,但是在我们的代码中,aux()声明中的类型TO参数会衰退为指针类型,因此对一些FROM类型允许true的结果
- 转换为函数类型应该总是产生false,但同数组的情况一样,我们的实现会把他们视为衰退类型
- 转换为(cv限定符)void类型应该总是产生true,但我们的实现甚至不会成功实例化TO为void类型的情况,因为参数类型不能有类型void
- 对于这些情况,需要附加的局部特化,然而对每个可能的cv限定符组合添加特化很笨拙。我们可以如下添加一个附加的模板参数给辅助类模板
template<typename FROM, typename TO, bool = IsVoidT<TO>::value
||
IsArrayT<TO>::value
||
IsFunctionT<TO>::value>
struct IsConvertibleHelper {
using Type = std::integral_constant<bool,
IsVoidT<TO>::value && IsVoidT<FROM>::value>;
};
template<typename FROM, typename TO>
struct IsConvertibleHelper<FROM,TO,false> {
... // previous implementation of IsConvertibleHelper here
};
- 额外的布尔模板参数确保对所有特殊的情况都用到primary helper traits的实现,如果转换为数组或函数,或如果FROM是void而TO不是,它将产生false_type,但对两个void类型它将产生false_type。其他所有情况产生一个false实参对第三个参数,因此将选用局部特化
- 标准库提供了对应的type traits std::is_convertible<>
06 检查成员
- SFINAE-based traits的另一项技术,创建一个traits,它能确定给定的类型T是否有名为X的成员
6.1 检查成员类型
- 首先定义一个traits,它能确定给定的类型T是否有成员类型size_type
#include <type_traits> // defines true_type and false_type
// helper to ignore any number of template parameters:
template<typename...> using VoidT = void;
// primary template:
template<typename, typename = VoidT<>>
struct HasSizeTypeT : std::false_type
{
};
// partial specialization (may be SFINAE'd away):
template<typename T>
struct HasSizeTypeT<T, VoidT<typename T::size_type>> :
std::true_type
{
};
- 和之前predicate traits一样,定义通用的情况派生自std::false_type,因为默认一个类型没有成员size_type,这里只需要一个构造
typename T::size_type
- 这个构造当且仅当类型T有成员类型size_type时有效,这正是我们需要的。如果构造无效,SFINAE造成局部特化被丢弃,fall back到基本模板,否则局部特化是有效且被优先选择的。使用这个traits如下
std::cout << HasSizeTypeT<int>::value; // false
struct CX {
using size_type = std::size_t;
};
std::cout << HasSizeType<CX>::value; // true
- 注意如果size_type是private,HasSizeTypeT产生false,因为traits模板无法访问实参类型,typename T::size_type是无效的,换句话说,traits测试是否有可访问的成员类型size_type
6.1.1 处理引用类型
- 引用类型总能出现许多问题,比如下面代码可以正常工作
struct CXR {
using size_type = char&; // Note: type size_type is a reference type
};
std::cout << HasSizeTypeT<CXR>::value; // OK: prints true
- 下面代码失效
std::cout << HasSizeTypeT<CX&>::value; // OOPS: prints false
std::cout << HasSizeTypeT<CXR&>::value; // OOPS: prints false
- 可以用RemoveReference traits移除引用
template<typename T>
struct HasSizeTypeT<T, VoidT<RemoveReference<T>::size_type>>
: std::true_type {
};
6.1.2 插入式类名称
- 同样值得注意的是检查成员类型的trati技术对插入式类名称也将产生一个true值
struct size_type {
};
struct Sizeable : size_type {
};
static_assert(HasSizeTypeT<Sizeable>::value,
"Compiler bug: Injected class name missing");
- static断言成功,因为size_type引入自己的名称作为一个成员类型,名称是继承的。如果不成功,我们将在编译器中发现一个缺陷
6.2 检查任意成员类型
- 定义一个像HasSizeTypeT的traits引发一个问题——如何参数化traits使其可以检查任何成员类型名称。不幸的是这点目前只能通过宏实现,因为没有语言机制来描述一个隐藏的名称,最接近的不使用宏的技术是使用泛型lambda。下面宏将工作
// traits/hastype.hpp
#include <type_traits> // for true_type, false_type, and void_t
#define
DEFINE_HAS_TYPE(MemType) \
template<typename, typename =
std::void_t<>> \
struct
HasTypeT_##MemType \
: std::false_type {
}; \
template<typename
T> \
struct HasTypeT_##MemType<T, std::void_t<typename
T::MemType>> \
: std::true_type { } // ; intentionally skipped
- 每个DEFINE_HAS_TYPE(MemberType)的使用定义一个新的HasTypeT_MemberType traits。比如可以用它检查一个类型是否有一个value_type或一个char_type成员类型
// traits/hastype.cpp
#include "hastype.hpp"
#include <iostream>
#include <vector>
DEFINE_HAS_TYPE(value_type);
DEFINE_HAS_TYPE(char_type);
int main()
{
std::cout << "int::value_type: "
<< HasTypeT_value_type<int>::value << '\n';
std::cout << "std::vector<int>::value_type: "
<< HasTypeT_value_type<std::vector<int>>::value << '\n';
std::cout << "std::iostream::value_type: "
<< HasTypeT_value_type<std::iostream>::value << '\n';
std::cout << "std::iostream::char_type: "
<< HasTypeT_char_type<std::iostream>::value << '\n';
}
6.3 检查非类型成员
- 也可以修改traits检查数据成员和单个成员函数
// traits/hasmember.hpp
#include <type_traits> // for true_type, false_type, and void_t
#define
DEFINE_HAS_MEMBER(Member) \
template<typename, typename = std::void_t<>>
\
struct HasMemberT_##Member
\
: std::false_type { };
\
template<typename T>
\
struct HasMemberT_##Member<T,
std::void_t<decltype(&T::Member)>> \
: std::true_type { } // ; intentionally skipped
- 这里使用SFINAE使&T::Member无效时局部特化失效,构造有效必须满足下列条件
- Member必须无歧义地定义一个T成员(比如不能是一个重载成员函数名称,或同名的多继承成员名称)
- 成员必须可访问
- 成员必须是一个nontype,nonenumerator成员(否则前缀&将会无效)
- 如果T::Member是一个静态数据成员,它的类型必须未提供一个operator&使得&T::Member无效(比如使那个操作符不可访问)
- 使用模板如下
// traits/hasmember.cpp
#include "hasmember.hpp"
#include <iostream>
#include <vector>
#include <utility>
DEFINE_HAS_MEMBER(size);
DEFINE_HAS_MEMBER(first);
int main()
{
std::cout << "int::size: "
<< HasMemberT_size<int>::value << '\n';
std::cout << "std::vector<int>::size: "
<< HasMemberT_size<std::vector<int>>::value << '\n';
std::cout << "std::pair<int,int>::first: "
<< HasMemberT_first<std::pair<int,int>>::value << '\n';
}
- 不难修改局部特化排除&T::Member不是pointer-to-member类型的情况,类似地,一个pointer-to-member函数能被排除或需求来限制traits、数据成员或成员函数
6.3.1 检查成员函数
- 注意HasMember traits只检查单个成员的名称,如果两个成员存在,比如检查重载的成员函数,traits会失效
DEFINE_HAS_MEMBER(begin);
std::cout << HasMemberT_begin<std::vector<int>>::value; // false
- 然而,如之前解释的,SFINAE原则禁止在一个函数模板声明中尝试创建无效类型和表达式,允许重载技术扩展为测试是否任意表达式是well-formed
- 因此,可以简化检查是否能调用一个对特定方式感兴趣的函数,甚至函数被重载也可行。和IsConvertibleT traits一样,这个trick是构建一个表达式,用于检查是否能调用附加的函数模板参数的默认值的decltype表达式中的begin(),即下面的decltype(std::declval<T>().begin())
// traits/hasbegin.hpp
#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and void_t
// primary template:
template<typename, typename = std::void_t<>>
struct HasBeginT : std::false_type {
};
// partial specialization (may be SFINAE'd away):
template<typename T>
struct HasBeginT<T, std::void_t<decltype(std::declval<T>().begin())>>
: std::true_type {
};
6.3.2 检查其他表达式
- 可以将上述技术用于其他表达式,甚至组合多个表达式,比如检查T1和T2类型之间是否有operator<
#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and void_t
// primary template:
template<typename, typename, typename = std::void_t<>>
struct HasLessT : std::false_type
{
};
// partial specialization (may be SFINAE'd away):
template<typename T1, typename T2>
struct HasLessT<T1, T2,
std::void_t<decltype(std::declval<T1>()<std::declval<T2>())>>
: std::true_type
{
};
- 这里的挑战也是定义一个有效的表达式,检查和使用decltype将其置于SFINAE context,如果表达式无效将造成到基本模板的fallback
decltype(std::declval<T1>() < std::declval<T2>())
- 这样检查有效表达式的traits是鲁棒的:只有当表达式是well-formed时产生true,当operator<歧义、被删除、不可访问都返回false。可以如下使用这个traits
HasLessT<int, char>::value // yields true
HasLessT<std::string, std::string>::value // yields true
HasLessT<std::string, int>::value // yields false
HasLessT<std::string, char*>::value // yields true
HasLessT<std::complex<double>,
std::complex<double>>::value // yields false
- 可以使用这个traits来要求模板参数T支持operator<
template<typename T>
class C
{
static_assert(HasLessT<T>::value,
"Class C requires comparable elements");
...
};
- 注意,由于std::void_t的性质,可以在一个traits中组合多个限制
#include <utility> // for declval
#include <type_traits> // for true_type, false_type, and void_t
// primary template:
template<typename, typename = std::void_t<>>
struct HasVariousT : std::false_type
{
};
// partial specialization (may be SFINAE'd away):
template<typename T>
struct HasVariousT<T, std::void_t<decltype(std::declval<T>().begin()),
typename T::difference_type, typename T::iterator>>
: std::true_type
{
};
6.4 使用泛型lambda检查成员
- isValid lambda提供了一个更紧凑的技术来定义检查成员的traits,帮助避免使用宏处理任意名称的成员
- 下例阐述了如何定义traits检查是否一个数据或类型成员,如first或size_type存在,或是否两个不同类型对象的operator<被定义
// traits/isvalid1.cpp
#include "isvalid.hpp"
#include<iostream>
#include<string>
#include<utility>
int main()
{
using namespace std;
cout << boolalpha;
// define to check for data member first:
constexpr auto hasFirst
= isValid([](auto x) -> decltype((void)valueT(x).first) {});
cout << "hasFirst: " << hasFirst(type<pair<int,int>>) << '\n'; // true
// define to check for member type size_type:
constexpr auto hasSizeType
= isValid([](auto x) -> typename
decltype(valueT(x))::size_type {});
struct CX {
using size_type = std::size_t;
};
cout << "hasSizeType: " << hasSizeType(type<CX>) << '\n'; // true
if constexpr(!hasSizeType(type<int>)) {
cout << "int has no size_type\n";
...
}
// define to check for <:
constexpr auto hasLess
= isValid([](auto x, auto y) -> decltype(valueT(x) < valueT(y)) {});
cout << hasLess(42, type<char>) << '\n'; // yields true
cout << hasLess(type<string>, type<string>) << '\n'; // yields true
cout << hasLess(type<string>, type<int>) << '\n'; // yields false
cout << hasLess(type<string>, "hello") << '\n'; // yields true
- 再次注意hasSizeType使用std::decay从传递的x移除引用,因为不能从一个引用访问一个类型成员,如果跳过这点,traits将总会产生false,因为第二个重载的isValidImpl<>()被使用
- 为了可以使用常见的泛型语法,把类型当作模板参数,可以再次定义附加的辅助
// traits/isvalid2.cpp
#include "isvalid.hpp"
#include<iostream>
#include<string>
#include<utility>
constexpr auto hasFirst
= isValid([](auto&& x) -> decltype((void)&x.first) {});
template<typename T>
using HasFirstT = decltype(hasFirst(std::declval<T>()));
constexpr auto hasSizeType
= isValid([](auto&& x)
-> typename std::decay_t<decltype(x)>::size_type {});
template<typename T>
using HasSizeTypeT = decltype(hasSizeType(std::declval<T>()));
constexpr auto hasLess
= isValid([](auto&& x, auto&& y) -> decltype(x < y) {
});
template<typename T1, typename T2>
using HasLessT
= decltype(hasLess(std::declval<T1>(), std::declval<T2>()));
int main()
{
using namespace std;
cout << "first: " << HasFirstT<pair<int,int>>::value << '\n'; // true
struct CX {
using size_type = std::size_t;
};
cout << "size_type: " << HasSizeTypeT<CX>::value << '\n'; // true
cout << "size_type: " << HasSizeTypeT<int>::value << '\n'; // false
cout << HasLessT<int, char>::value << '\n'; // true
cout << HasLessT<string, string>::value << '\n'; // true
cout << HasLessT<string, int>::value << '\n'; // false
cout << HasLessT<string, char*>::value << '\n'; // true
}
- 现在
template<typename T>
using HasFirstT = decltype(hasFirst(std::declval<T>()));
- 允许调用
HasFirstT<std::pair<int,int>>::value
网友评论