C++的模板是个争议很大的特性,有些公司的编码规范明确禁止使用template(STL之类的不受限制),理由是C++模板的报错信息非常不友好,另外不能以二进制方式复用,模板代码一旦变化就会扩散至所有使用模板的代码中、维护成本高。
template可以极大地方便SDK、公共库等岗位的开发人员,对侧重业务实现的开发人员来说可能没有那么有吸引力,日常使用中与模板有关的最多的可能就是STL了。
vector<int> int_vec;
vector<float> float_vec;
.......
除此之外,模板其实还有很多特性值得记录。
1. 模板的实例化(instantiation)与特化(specialization)
模板的特化(template specialization)在很多地方被翻译成具现化、具体化、具象化,个人认为这三个翻译不太好,将specialization翻译为“特化”简单明了——具体化等容易与实例化傻傻分不清楚。
模板的实例化是根据类型信息生成模板函数或者模板类,实例化的结果是一个具体的函数或者类;而特化的结果则依然是一个模板(此种说法有失准确:全特化的结果也是一个具体的函数或者类,偏特化的结果是一个模板,此处的这种说法只是为了方便区分实例化与特化的主要区别。在《C++ Template中文版》一书P84中有指出,“实例化并不是产生特化的唯一方式”,可见,严格来说,实例化也是特化的一种)。
实例化又分为两种:隐式实例化、显式实例化。由编译器自动推断类型信息、进行实例化,称之为隐式实例化。由程序员用尖括号指定类型信息,称之为显式实例化。
特化也分为两种:全特化与偏特化。特化时指定所有的模板参数,称之为全特化(有些地方译为完全特化、偏特化有些地方译为局部特化、部分特化)。
在C++17之前,类模板只能进行显式实例化。C++17开始支持类模板的实参推演(template argument deduction,有些地方翻译为模板实参演绎、有些地方译为推演,个人觉得推演更好)——类模板也可以进行隐式实例化了。
函数模板不支持偏特化。
template <typename T, typename U>
class ClassT{//基本模板
public:
ClassT(const T& t, const U& u){
std::cout<<"general template"<<std::endl;
}
};
template <>
class ClassT<char, int>{//全特化
public:
ClassT(const char& t, const int& u){
std::cout<<"specialized template"<<std::endl;
}
};
template <typename U>
class ClassT<bool, U>{//偏特化
public:
ClassT(const bool& t, const U& u){
std::cout<<"partial specialized template"<<std::endl;
}
};
template<typename T, typename U>//基本函数模板
void func(const T& t, const U& u){
std::cout<<"general template"<<std::endl;
}
template<>//函数模板的全特化
void func<int, float>(const int& t, const float& u){
std::cout<<"specialized template"<<std::endl;
}
//类模板的显式实例化
ClassT<float, float> instance1(1.0, 2.0);
//类模板的隐式实例化,C++17后才支持(得益于Class template argument deduction)
auto instance2 = ClassT{'a', 10};
//类模板的隐式实例化,C++17后才支持
ClassT instance4(false, 1.0);
//函数模板的隐式实例化
func(1,1.0);
//函数模板的显式实例化
func<int, float>(2,2.0);
2,duck_typing、静多态
在众多语言中,都采用继承接口、继承基类这一方式来实现is_a的效果。假如我们定义鸭子是会叫的、走路时一扭一扭的,那么要想让别的类知道你这个类实现了鸭子的特性,你必须这样干:
class IDuck{
void quack(const string& str) = 0;
void walkTwist(const int& distance) = 0;
}
class Duck : public IDuck{
void quack(const string& str){ ... }
void walkTwist(const int& distance){ ... }
}
void playWithDuck(IDuck* pDuck){ ... }
只有继承了IDuck,实现了里面的方法,你的对象才能够被允许传入playWithDuck函数。这种方式是基于类型的、侵入式的(Duck必须继承IDuck)。
C++模板允许另外一种实现方式——duck typing。
python、go等语言都支持duck typing。所谓的duck typing,通俗的来说,就是“当我看到一只鸟走路像鸭子,叫声像鸭子,那我就把它当做鸭子”
我们看用C++模板的效果:
class Joker{
void quack(const string& str){ ... }
void walkTwist(const int& distance){ ... }
void someOtherFunc(){ ... }
}
class Duck{
void quack(const string& str){ ... }
void walkTwist(const int& distance){ ... }
}
template <typename DuckType>
void playWithDuck(DuckType* pDuck){
pDuck->quack("gagaga");
pDuck->walkTwist(10);
}
Joker joker;
Duck duck;
playWithDuck(&joker);
playWithDuck(&duck);
Joker类,实现了quack方法和walkTwist方法,那么就可以认为joker也是一只鸭子,也可以被playWithDuck方法使用。而Joker类并没有继承任何东西,这种is-a是基于特征的、非入侵的、松散的。
广义来讲,凡是能够实现“一种调用,不同实现”这种效果的,都算是多态,所以上面代码中的playWithDuck应该也算是多态了。按照对“实现”的绑定时机,又细分出了静多态和动多态。动多态无疑就是虚函数表那一套了,在运行时通过vptr找到v_table,然后通过偏移找到函数地址——这些过程是发生在运行时的——动多态。playWithDuck则是在编译期对实现进行绑定,算是静态多态。
当C++11发布后,C++多态就有了更进一步的变化:因为有了std::function 与 std::bind。
我们前面说C++模板实现多态是基于特征的——换句话说,是基于函数名的。当有了function/bind这对兄弟之后,C++实现多态就更加抽象了——基于函数签名(函数的返回值、参数列表)。也就是说,实现嘎嘎叫的方法不一定非得叫quack:
class Joker{
void quack(const string& str){ ... }
void walkTwist(const int& distance){ ... }
void someOtherFunc(){ ... }
}
class Joker2{
void gagaga(const string& str){ ... }
void walkTwist(const int& distance){ ... }
void someOtherFunc(){ ... }
}
void talkToDuck(std::function<void(const string&) quackFunc>{
quackFunc("gagaga");
}
Joker joker;
Joker2 joker2;
auto quackFUnc1 = std::bind(joker, Joker::quack);
auto quackFunc2 = std::bind(joker2, Joker2::gagaga);
talkToDuck(quackFUnc1);
quackFunc2(joker2);
我们可以发现,talkToDuck函数的参数只对返回值、参数列表做了要求,至于进来的函数名叫啥根本不关心。在调用talkToDuck之前用bing装配一下,多态的效果便有了。
std::function与std::bind值得单独整理一篇博客。
3,奇异递归( Curiously Recurring Template Pattern,CRTP)
奇异递归可以让基类的代码使用子类的方法,同时也能够实现静态多态:
template<typename Drived>
class Base{
public:
void open(){
reinterpret_cast<Drived*>(this)->open_impl();
}
}
class DrivedA:public Base<DrivedA>{
public:
void open_impl(){ ... }
}
class DrivedB:public Base<DrivedB>{
public:
void open_impl(){ ... }
}
DrivedA driveA;
driveA.open();
DrivedB driveB;
driveB.open();
上面的代码“奇异”在哪里呢?
class DrivedA:public Base 这行。
子类在继承父类的时候,把自己作为模板参数传给了父类。这种形式能够使父类可以调用子类的方法。
4,编译期断言
C++的template是图灵完备的,利用模板的偏特化这个特性即可实现if判断,而编译期断言就是利用偏特化实现在编译期判断某个编译期表达式(常量表达式)的结果:
在C++11之前,可以这样实现一个static_assert
template<bool>
struct ComplileTimeChecker{
ComplileTimeChecker(...);
};
template<>
struct ComplileTimeChecker<false> {};
#define CTASSERT(expr, msg) { \
class ERROR_##msg {}; \
ERROR_##msg MsgMaker();\
sizeof(ComplileTimeChecker<expr>(MsgMaker()));\
}
//报编译错误:error: no matching function for call to 'ComplileTimeChecker<false>::ComplileTimeChecker(main()::ERROR_this_is_error_msg)'
CTASSERT(false, this_is_error_msg)
CTASSERT(true, this_is_error_msg)
当expr的值为false的时候,编译器匹配到特化后的类模板,该模板取消了带参数的构造函数,会报编译错误,并带上第二个参数中的错误信息。并且,这个实现没有运行时的开销,sizeof的一切活动都在编译期进行。
5,SFINAE(Substitution Failure Is Not An Error 匹配失败非错误)
刚开始看到这个名词的时候一头雾水——什么匹配失败?什么非错误?
我们先看他是啥,再看他有什么用处。
class A{}
class B{}
class C{}
void func(A a){}
void func(B b){}
A a;
B b;
C c;
func(a);
func(b);
func(c);
因为func有重载,当编译器为func(b)匹配函数实现的时候,会先尝试匹配void func(A a)这个函数,当然会匹配失败因为类型不对,但是匹配失败并不会报编译错误,而是会继续往下匹配,尝试void func(B b)这个函数。
对func( c )调用而言,因为匹配完所有代码发现没有一个匹配的上,此时才会发出编译错误。
对于模板函数的匹配也有上述处理过程。说白了,SFINAE使编译器能够为一个函数调用匹配到最合适的函数实现,匹配过程中遇到的那些匹配失败的函数实现,编译器不会报错,会跳过继续匹配;当编译器经历完所有的匹配失败后仍然没有匹配到,才报编译错误。
再说白一点,这个特性能够让编译器在编译期进行类型处理、计算。
澄清:上述代码只是体现了“失败非错误”这一特点,严格意义上来讲,substitution是指函数模板实参替换形参的匹配过程,上述代码未涉及到模板,只是为了更直观的解释“失败非错误”。失败非错误这一特性,也并非只存在于substitution阶段。关于substitution、template argument deduction、template instantiation、overload resolution,挖个坑另起一篇博客吧。
现在基本弄明白SFINAE是啥了,我们看下他有啥妙用:
template<typename T>
class IsClassT {
private:
typedef char One;
typedef struct { char a[2]; } Two;
template<typename C> static One test(int C::*);
// Will be chosen if T is anything except a class.
template<typename C> static Two test(...);
public:
enum { Yes = sizeof(IsClassT<T>::test<T>(0)) == 1 };
enum { No = !Yes };
};
class TestClass{
};
IsClass<TestClass>::Yes; //true
IsClass<int>::Yes; //false
当使用IsClassT::test(0)实例化IsClass类模板的test成员函数模板时,会先匹配到“One test(int C:: * )”,因为根据重载解析规则,将0转为空指针常量要优先于将实参绑定到省略号,所以此处test的参数0将更优先匹配到IsClassT::test(0)这个重载。
另外,int C:: * 这个形参也很容易让人费解:它声明的是一个int型的成员指针,我们的TestClass是一个空类——没有任何成员,但是最终的结果依然是该空类能够匹配到IsClassT::test(0), 原因是:SFINAE允许试图创建无效的类型,此处的关键是成员指针,而不是成员指针指向的类型。
6,可变参数模板与折叠表达式
template<typename T>
T mySum(const T& a, const T& b){
return a + b;
}
template<typename T, typename... Args>
T mySum(const T& a, const Args&... restArgs){
return a + mySum(restArgs...);
}
template<typename... Args>
auto mySum2(const Args&... args){
return (args + ...);
}
int main(){
std::cout<<mySum(1,2,3,4,5)<<" "<<mySum2(4,5,6,7,8);
}