C++模板的一些整理

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 DrivedApublic Base<DrivedA>{
public:
	void open_impl(){ ... }
}
class DrivedBpublic 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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值