C++模板黑科技

模板只有类模板和函数模板,以下是各种黑科技。

 

①把实现的模板类或函数拆分到h和cpp中

    首先需要明确,传统上我们在h中写完整的definition,对于包这个头文件的模板使用者来说,编译器模板展开后就是内联的用法。

    如果我们只在h中写declaration,对于包这个头文件的模板使用者,展开后就是使用的声明,编译阶段没有任何问题;在链接阶段会去链接展开后的实现的。

// A.hpp
class A {
public:
        template <bool isPrepareStage>
        void consume(int64_t size);
}

 

// A.cpp
#include "A.hpp";
template <bool isPrepareStage>
void A::consume(int64_t size) {
    // 实现
}
template void A::consume<true>(int64_t);
template void A::consume<false>(int64_t);

    这个例子演示了模板函数(方法)拆开declaration和definition的一种方式,主要就是在cpp中用template前缀显式地生成若干种展开后的模板函数(方法),编译到该cpp的object文件中,这样可以被其他的调用者链接上。

    模板类的拆分方法也类似。

 

顺便说下C++的内联

    C++标准规定,任何使用内联的地方,都要有该内联函数/方法的完整而相同的definition(如果definition不一样最后程序的行为是未知的)。

    要实现这一点,我们常用的方法就是把内联函数/方法的definition全部写在头文件里(正如我们写模板时常用的那样)。而且实现在头文件时,对于方法来说,不用写inline关键字就是内联的;对于函数,则必须使用inline关键字(否则就违反了C/C++的一次定义原则,编译器只会对inline的函数来规避重复定义的问题)。

    很多代码洁癖患者在实现类的时候,把方法照旧拆分到h和cpp中,但是又都标记上内联,这是又想马儿跑又想马儿不吃草。如果这个方法只有这个cpp用,那么没啥问题,正常内联cpp内的实现;如果别人包了头文件,使用这个方法的时候却只有一个inline的方法declaration,所以编译的时候就会报错了,因为这时候顶多只能走后期的链接,无法展开代码来内联。

    另外C++标准还规定,内联的写法对于编译器只是一个“建议”,具体是否要内联,完全是编译器自己优化决定;甚至有些本来不内联的方法,编译器反而会内联,你标记了内联的方法,编译器却走了传统的链接。

 

②模板类的静态成员变量初始化

例如一个面试题:请写一个singleton的实现,最好用模板或者继承让它更有扩展性,最好能线程安全。
答:模板的实现没写出来主要在于模版类的静态变量的初始化不会写,格式是这样的(忽略线程安全)

template<typename T>
class Singleton {
public:
    static T* getInstance() {
        if (!ms_ins) {
            ms_ins = new T();
        }
        return ms_ins;
    }
private:
    static T* ms_ins;
};
template<typename T>
T* Singleton<T>::ms_ins = NULL;  // 依然是写在头文件里面

    包含了这个头文件之后,调用Singleton<ClassA>::getInstance()的时候,编译器就展开了这个类的定义和方法的内联定义,从而通过编译;猜测这时候并没有静态成员初始化的展开,编译器应该会记录所有实例化这个Singleton模板的类,然后在链接之前对于每个实例化的类,只展开一份该初始化的代码进行编译,从而链接成功。

    另外因为C++11规定了static局部变量是线程安全的,那么完全可以用这个来实现,lazy initialize并且能保证在程序关闭的时候会调用instance的析构。

 

③特例化或显式定义

这分为两个部分,一个是编写模板类或者函数的时候,一个是实例化模板类或者函数的时候。

 

首先是编写的时候:

我们在之前的文章中已经见识过以下的模板函数特例化了:

 

template<typename T>
void fn(T) {
	std::cout<<"template function"<<std::endl;
}

template<>
void fn(const std::string&){
	std::cout<<"normal function"<<std::endl;
}
其实这是一个简化的写法。无论是编写模板类还是模板函数,假设之前是template<typename T> XXX...,要特例化的时候,例如特例化int的版本,应该写成template<> XXX<int>...。

 

之所以例子中可以简化掉fn后面的<const std::string&>,是因为可以从参数列表推导出来。如果T不是用在参数列表里面,就得写全了,下文紧接着会说明。

 

另外在编写特例化的模板类时,往往我们会感觉存在大量的共通代码,特例化的只是几个方法,这种时候有两种基于继承来共享代码的方案。一种是共享代码位于同一个父类,子类来特例化成不同版本,不过似乎继承模板父类时,在子类中要访问父类的成员,必须得前缀父类的名字;一种是共享代码位于同一个子类,而特例化不同版本的父类,并且通过CRTP来访问子类中共通代码,这时候我们往往需要http://en.cppreference.com/w/cpp/language/partial_specialization这种部分模板参数特例化的方式来特例化父类(特例化原本的模板参数,保留CRTP模板参数不变)。

 

然后是实例化的时候:

template <typename T>
void func() {
    cout << "normal version" << endl;
}
template <>
void func<int>() {
    cout << "explicit speciallized version" << endl;
}

int main(int , char* []) {
    func<double>();
    func<int>();
    return 0;
}

 这次的func就是上文说的T不在参数列表里,因此在实例化的时候,就像跟实例化类似的,要在func<>中写明要实例化的T的类型。同时我们也看到在编写func的时候,特例化int版本的时候,也需要在func<>中写上int。

 当然这种模板函数(template<typename xxx>)比较少见,更常见的是上文①中用template<bool xxx>或者template<size_t xxx>这种基本类型形式的模板,需要显式实例化(正如①中演示的)。

 用bool一般是考虑编译器可以优化成两个函数,避免了分支跳转;用size_t一般是用来传入数组的长度之类的编译期信息,例如在此文中提到的_countof的实现,甚至还避免了显式的实例化。

 

你可能感兴趣的:(C++模板黑科技)