天天看点

c++ 模板总结

模板

模板被称为c++ 下的编译器多态。最近写模板遇到了一些坑总结下。

模板 声明与定义分离

c++ 惯有的源文件模式都是 将声明写到 .h 文件中,将实现写到 .cpp 文件中。但是对于模板来说,如果我们把 模板类或者模板函数这样分离实现,当链接的时候会报链接错误。

本质上,这个错误因为 当其他源文件 包含 模板的头文件的时候,编译器编译当前源文件时会认为该 模板 已经有了相应实现,具体的实现就在相应的模板源文件中,编译器不会再去实例化相应的模板函数 或者 模板的成员函数 ,它会交给 链接器去链接。

这个时候因为虽然 模板的源文件有了 相应的实现,但是如果 没有使用相应的模板函数 或者 模板类 的话, 编译器并不会给该模板源文件的模板进行实例化 ,所以等链接器链接的时候会报链接错误,如下demo。

templateA.h 
template <class T>
class TemplateA {
 public:
  void Fun();
};
templateA.cpp
template <class T>
void TemplateA<T>::Fun() {} 

main.cc 
int main() 
{
  templateA<int> test_a;
  test_a.Fun();
  return 0;
}
main.cc:(.text+0x32): undefined reference to `TemplateA<int>::Fun()
           

所以解决方案俩种

  1. 将模板的实现和定义全放到头文件中
  2. 在 .cpp 中使用 template 关键字进行显式实例化
templateA.cpp 
template <class T>
void TemplateA<T>::Fun() {} 

//显示实例化
template class TemplateA<int>;
           

但是显示实例化虽然一定程度实现了 声明与实现分离,但是这样依赖于调用者的实现,每多一个类型, template.cpp 文件就得多一行相应的实例化声明,接着相应的 template.cpp 就得被重新编译。

派生类继承模板类

有的时候我们想 继承一个模板类的具体的实例化,这样是可以的。当需要重写 模板类的虚函数时,只要给出具体类型的实现即可。

templateA.h 
template <typename Key>
class Table {
  public:
    Table() {}
     
    virtual Fun (Key key) = 0;
};

main.cc 
class WoodTable : public Table<int> {
  public:
    WoodTable()
     {}
 
    virtual Fun(int key) 
    {}
};
           
模板函数类型推导

参数推导顺序 从 右 到 左 参数以此进行推导。

模板类中的虚函数

  1. 模板类的成员函数是可以是 虚函数的。
  2. 但是 成员模板函数不能是虚函数

    这俩个很绕口,模板类的成员函数,它可以使用 模板类的模板参数,并且可以虚化(也就是可以是虚函数)。但是在模板类 / 类中定义一个 成员模板函数,把它声明成 虚函数,编译器会报错。

模板类的成员函数虚化

下面的这个模板类的成员函数,虽然它里面也使用了模板类的模板参数,但是它也是可以进行虚拟化的。

BaseTemplate.h 
#ifndef BASE_TEMPLATE_H
#define BASE_TEMPLATE_H

template <class T>
class Base {
 public:
  Base()
  {}
  
  virtual void Fun(T arg) {}  // 合法
};

#endif

Dervied.h 
#ifndef DERVIED_H
#define DERVIED_H

#include "base_template.h"

class Dervied : public Base<int> {
 public:
  Dervied()
  {}

  virtual void Fun(int arg)
  {
    ++arg;
  }
};

#endif
           
成员模板函数虚化

下面这个虽然也是使用到了一个模板参数,但是由于它是一个模板函数,编译器会报错不允许虚化。其实以上原因是由于模板类的虚表的创建是在第一个实例化的非纯虚函数的类中创建的。当模板类进行是实例化的时候,该成员函数也会进行相应的实例化,所以它可以很快的加到虚表中。但是成员模板函数不同,即使相应的模板类实例化的时候,相应函数还是模板,如果想知道有多少确切个相应由 该模板函数产生的虚函数,编译器需要扫描所有的 .cpp 文件,又因为c++ 是分离编译,这样很不现实,所以成员模板函数不可以虚化。

BaseTemplate.h 
#ifndef BASE_TEMPLATE_H
#define BASE_TEMPLATE_H

template <class T>
class Base {
 public:
  Base()
  {}
  
  template <class D>
  virtual void Fun(D arg) {}  // 非法
};

#endif

Dervied.h 
#ifndef DERVIED_H
#define DERVIED_H

#include "base_template.h"

class Dervied : public Base<int> {
 public:
  Dervied()
  {}

  virtual void Fun(int arg)
  {
    ++arg;
  }
};

#endif
           
模板类的虚表

模板类公用一张虚表,该虚表存在于第一个实例化且无纯虚函数的类中。(c++对象模型)

编译器如何编译模板函数

写模板函数分离编译的时候会遇到链接错误。经过分析编译器编译模板函数,得到以下现象。当头文件中有模板函数的实现的时候,具体的.cpp 文件调用模板函数的时候,经编译器编译生成的.o 文件中,模板函数在符号表中的Bind属性是 weak 属性,在相应的.o 文件中并没有 模板函数的实例,也就是并没有真正的编译模板函数在实例化的.cpp文件中。

当使用分离编译的时候,.h 只保留 模板函数的声明,实现放到相应的.cpp文件中。在实例化的.o文件中,模板函数 Bind为 Strong,所以链接的时候会出现链接错误。

对于weak 属性,经过查资料,weak的符号可以有多个,链接的时候链接器只会绑定一个,并且即使没有实现weak符号的函数,也可以生成程序,只不过 f 地址为NULL

void __attribute__((weak)) f();
int main(void)
{
        if (f) {
          f();
        }
        return 0;
}
           

如上面这个列子,所以我在想gcc 是不是遇到模板函数的实例化的时候,压制了相应代码的生成,等到链接的时候再返回去编译模板函数,再一一链接模板函数给相应的实例化 cpp文件。

type_traits

类型萃取主要使用了模板特化的特性。c++ 03 标准库中虽然没有实现相应的类型萃取,但是在STL内部却有很多使用了,比如STL的空间配置器,它就是使用了类型萃取的一些技巧去实现一些函数。在 c++ 11中,类型萃取这一技巧也放到了标准库中。

模板特化

下面这个IsPod 类就是使用了模板的特化实现了类型萃取。因为特化的存在,我们可以完全通过特化某个具体类型实现一个与大众独一无二的类出来,所以有了这个东西,我们就可以来进行类型萃取,来可以在编译期间就可以知道T的类型,从而在运行期间进行一些选择。比如c++空间配置器的 Construct 函数,通过IsPod来进行类型萃取,萃取出来内置类型通过 memset 来实现,萃取出来ADT类型就for循环调用置位new表达式来实现相应构造。

包括TMP的一些东西也都是使用了模板特化

内置类型 的萃取demo

struct TrueType {
};
struct FalseType {
};

template <class T>
struct IsPod {
struct FalseType type;
};
// 全特化
template <>
struct IsPod<int> {
struct TrueType type;
};
template <>
struct IsPod<char> {
struct TrueType type;
};
...
           

指针类型萃取demo

template <class T>
struct IsPointer {
struct FalseType type;
};
// 偏特化
template <>
struct IsPointer<T*> {
struct TrueType type;
};
           
TMP

tmp可以在编译期期间为我们做一些运算。

template <int Size>
struct Fib {
 enum { type = Fib<Size-1>::type + Fib<Size-2>::type};
};
template <>
struct Fib<1> {
 enum { type = 1};
};
template <>
struct Fib<2> {
 enum { type = 2};
};