本文来自Modern C++ Design的第二章:技术,会记录以下技术内容:

  • Partial template specialization,模板偏特化

  • Local Classes,局部类

  • 型别与数值间的映射性(Int2type Type2Type class templates)

  • 编译期察觉可转换性和继承性

  • 型别信息及一个容易上手的std::type_info外覆类(wrapper)

  • Select class template,编译器根据bool状态选择型别;

  • Traits,traits技术集合

2.1 编译期错误Compile-Time Assertions及定制的错误日志

(注:代码复现价值并不大,因为现在的编译器能直接检出reinterpret_cast的错误转换,报错日志和原书不同;)

考虑一段实现安全转型的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <assert.h>
using namespace std;

template <typename To,typename From>
To safe_reinterpret_cast(From from){
assert(sizeof(From)<=sizeof(To));
return reinterpret_cast<To>(from);
}

int main(){
int i = 5;
safe_reinterpret_cast<char*>(i);

return 0;
}

这段代码通过assertion避免了类型的下行转换,然而assertion是在运行期发生的,有这么一个理由有必要将错误提前到编译期:reinterpret_cast是不可移植的,reinterpret_cast会重新解释类型的比特含义,在不同平台下内存布局有差异,有机会导致结果的差异,总有办法来做到这一点,例如考虑通过创建数组形式,创建一个非正数数组是失败的:

1
2
3
4
5
6
7
8

#define CHECK(exp) int array[exp?1:0]

template <typename To,typename From>
To safe_reinterpret_cast(From from){
CHECK(sizeof(From)<=sizeof(To));
return reinterpret_cast<To>(from);
}
现在错误指向了创建了一个非正个数数组,但是这样的报错很隐晦,偏特化的方式可以获得更直观的报错:'CastError<false> error' has incomplete type
1
2
3
4
5
6
7
8
template<bool>struct CastError;
template<>struct CastError<true>{};

template <typename To,typename From>
To safe_reinterpret_cast(From from){
CastError<sizeof(From)<=sizeof(To)> error;
return reinterpret_cast<To>(from);
}
加上错误信息,可以写成:
1
2
3
4
5
6
7
8
9
10
11
#define CHECK(exp,msg) \
{\
class CastError_##msg{};\
(void)sizeof(CastError<exp>(CastError_##msg()));\
}

template <typename To,typename From>
To safe_reinterpret_cast(From from){
CHECK(sizeof(From)<=sizeof(To),Param_Too_Narrow);
return reinterpret_cast<To>(from);
}

2.2 Partial template specialization

模板特化分成全特化和偏特化,也分成类模板特化和函数模板特化;本节原创。

全特化

当template参数列表为空,模板所有参数都确定,就是全特化,模板原型可以仅声明不定义,除非要用到原始模板:

1
2
3
4
5
6
7
template<typename T1, typename T2>
void func(T1 t1,T2 t2);

template<> //全特化
void func<int,double>(int t1,double t2){
cout<<"template<>"<<t1<<t2<<endl;
}

类的全特化也类似,值得一提的是,特化版本是一个相对独立的类,即模板类不会限制死特化版本的成员变量和函数,换言之你可以在特化版本不定义、新定义、修改成员变量和函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
template<typename T1,typename T2>
class Person{
public:
template<typename T3 = decltype(T1()+T2())>
T3 add(){
return a+b;
}

private:
T1 a;
T2 b;
};

template<> //全特化
class Person<int,double>{
public:
void add(){
cout<<"new add:"<<a+b+c<<endl;
}

void declare(){
cout<<"class Person<int,double>"<<endl;
}

private:
int a;
double b;
int c = 10;
};

模板类对特化类的一些限制有:

  1. 特化类是相对独立,不会继承模板类的成员函数/变量,你需要自行声明和重新实现它们;

  2. 特化指的是类特化,不能单独特化某一个成员函数,但可以单独特化模板成员函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33

    template<typename T1,typename T2>
    class Person{
    public:
    template<typename T3 = decltype(T1()+T2())>
    T3 add(){
    cout<<"template<typename T3 = decltype(T1()+T2())>"<<a+b;
    return a+b;
    }

    void declare(){ //不能被单独特化
    cout<<"eeeee"<<endl;
    }

    private:
    T1 a;
    T2 b;
    };

    template<>
    template<>
    int Person<int,double>::add(){
    cout<<"Template member-func"<<endl;
    return (int)(a+b);
    }

    // void Person<int,double>::declare(){ ////报错,不能单独特化成员函数!
    // cout<<"cccccc"<<endl;
    // }

    //main函数:
    Person<int,double>person1; //OK
    person1.add<int>(); //显示调用特化函数

  3. 特化版本应该显式调用,以便编译器能够获知函数由模板特化而来;

  4. 模板类和特化类必须在同一个命名空间中;

  5. 特化类中也许存在特化的函数,其中成员函数的特化仍然遵循不能是偏特化

偏特化

只有类存在偏特化行为,即仅确定一部分参数类型,总结起来有这样的用法:

原始模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T1,typename T2>
class Person{
public:
template<typename T3 = decltype(T1()+T2())>
T3 add(){
cout<<"template<typename T3 = decltype(T1()+T2())>"<<a+b;
return a+b;
}

private:
T1 a;
T2 b;
};

  1. 确定一部分模板类型是特化:

    1
    2
    3
    4
    5
    6
    template<typename T1>
    class Person<T1,double>{
    void declare(){
    cout<<"class Person<T1,double>"<<endl;
    }
    };

  2. 将两种类型特化成相同类型也是特化:

    1
    2
    3
    4
    5
    6
    template<typename T>
    class Person<T,T>{
    void declare(){
    cout<<"class Person<T,T>"<<endl;
    }
    };

  3. 这里不针对偏特化,全特化也有这种非类型的特化,例如将bool特化成true/false,将int特化成特定的数字等:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <typeinfo>
    #include <cxxabi.h> //for abi
    template <typename T,bool isBool, int Int>

    //模板版本:
    struct Test{
    Test(){
    cout <<"template <typename T,bool isBool, int Int>"<<endl;
    }
    };


    //特化版本:将Bool、Int特化
    template <typename T>
    struct Test<T,true,1>{
    Test(){
    int status;
    char* demangled = abi::__cxa_demangle(typeid(value).name(), nullptr, nullptr, &status); //取gcc类型名
    cout <<demangled<<" struct Test<T,true,1>"<<endl;
    }
    T value;
    };

当然,类型偏特化是最常用的,函数模板不支持偏特化,也可以通过重载完成这样的目的:

1
2
3
4
5
6
7
8
9
template<typename T1, typename T2>
void func(T1 t1,T2 t2){
cout<<"template<typename T1, typename T2>"<<t1<<t2<<endl;
}

// template<typename T1> //错误!函数不能偏特化
// void func<T1,int>(T1 t1,int t2){
// cout<<"func<T1,int>"<<t1<<t2<<endl;
// }
重写一个重载函数:
1
2
3
4
template<typename T2>
void func(int t1,T2 t2){
cout <<"template<typename T1>"<<t1<<t2<<endl;
}
加上刚刚全特化的版本,我们有了三个同名函数:
1
2
3
4
template<>
void func<int,double>(int t1,double t2){
cout<<"template<>"<<t1<<t2<<endl;
}
当你写下:
1
2
3
int t1 = 5;
double t2 = 9.999;
func(t1,t2);
哪个函数会首先被匹配呢?很清楚这个类型即满足全特化也满足偏特化,因此必然是其中一个,答案是偏特化版本会被首先适配,因为这里使用了隐式声明,编译器也不会选择更加匹配的类型推导去优先匹配全特化(毕竟int类型是非常匹配的),如果你真的需要优先调用全特化,应该显式调用
1
func<int,double>(t1,t2);