跳转至

模板基础

模板的编译检查

编译器对模板的编译检查是分两步的:

在模板定义阶段,模板的检查并不包含类型参数的检查。包含下面几个方面:

  • 语法检查。比如少了分号。
  • 使用了未定义的不依赖于模板参数的名称(类型名,函数名,......)。
  • 未使用模板参数的staticassertions。

在模板实例化阶段,为确保所有代码都是有效的,模板会再次被检查,主要是那些依赖于类型参数的部分,只要会发生模板参数替换的地方,都会进行检查。

template<typename T>
void foo(T t)
{
    // 如果 undeclared() 未定义,第一阶段就会报错,因为与模板参数无关
    undeclared();
    //如果 undeclared(t) 未定义,第二阶段会报错,因为与模板参数有关
    undeclared(t); 

    // 与模板参数无关,总是报错
    static_assert(sizeof(int) > 10,"int too small"); 
    //与模板参数有关,只会在第二阶段报错
    static_assert(sizeof(T) > 10, "T too small");  
}

类型推断中的类型转换

在模板匹配时会进行类型的推断,如:

template<class Tp>
void f(Tp x) {};

f(1);

这里模板参数 Tp 被推导为 int。在这样的类型推断中,自动的类型转换是受限的:

  • 如果形参是引用传值,那么任何类型转换都是不被允许的。

  • 如果形参是按值传递,那么会进行退化(decay)这一种简单的转换。

这里退化(decay)指:去 cv 限定符,去引用,原始数组(raw array)和函数转换为响应的指针类型。

template<typename T>
T max (T a, T b);
int const c = 42;
Int i = 1; 
max(i, c); //  T 被推断为 int,c 中的 const 被 decay 掉
max(c, c); //  T 被推断为 int
int& ir = i;
max(i, ir); //  T 被推断为 int, ir 中的引用被 decay 掉
int arr[4];
foo(&i, arr); //  T 被推断为 int*

模板特化

特化分为全特化和偏特化,全特化是将所有的模板参数都指明,偏特化是只指明部分模板参数。需要注意的是函数模板不能进行偏特化,只能进行重载,只有类模板可以进行偏特化。

函数模板

// 函数模板
template<class Tp1,class Tp2>
void f(Tp1,Tp2) { }

// 全特化
template<> 
void f(int,int) {}

// 函数重载, 这会成为一个独立的函数模板
template<class Tp1,class Tp2>
void f(Tp1*,Tp2*) { }

// 函数重载, 这不是偏特化,而是一个独立的函数模板
template<class Tp2>
void f(int,Tp2) { }

在上面调用上面的函数时,重载决议会优先选择普通函数,如果不存在符合的普通函数,就会进行模板函数的重载选择,选择重载的函数模板时,不会考虑各个模板的全特化版本,重载决议发生在主模板之间,决议出符合的主模板后,才会具体选择模板的全特化版本。

我们看下面这个例子:

// 函数模板
template<class Tp1,class Tp2>
void f(Tp1,Tp2) { std::cout << "f(Tp1,Tp2)" << std::endl;}

// 全特化 ,选择 f(Tp1,Tp2) 为主模板
template<> 
void f(int,int) { std::cout << "f(int,int)" << std::endl;}

// 函数重载, 这不是偏特化,而是一个独立的函数模板
template<class Tp2>
void f(int,Tp2) { std::cout << "f(int,Tp2)" << std::endl; }

int main()
{
    f('a','c'); // 调用 f(Tp1,Tp2)
    f(1, 2);    // 调用 f(int,Tp2)
    f(1, 'c');  // 调用 f(int,Tp2)
    return 0;
}
/*
输出结果:
f(Tp1,Tp2)
f(int,Tp2)
f(int,Tp2)
*/

我们发现 f(1,2) 调用的是 f(int,Tp2) ,这就是因为在选择主模板时就已经选择了 f(int,Tp2) ,而非 f(Tp1,Tp2) ,所以它的全特化版本 f(int,int) 就不会被调用。

这里要注意的是全特化版本的函数,在选择主模板时,会向上查找最符合的主模板函数,所以如果我们改一下 f(int,int) 的位置就可以调用到它了:

// 函数模板
template<class Tp1,class Tp2>
void f(Tp1,Tp2) { std::cout << "f(Tp1,Tp2)" << std::endl;}

// 函数重载, 这不是偏特化,而是一个独立的函数模板
template<class Tp2>
void f(int,Tp2) { std::cout << "f(int,Tp2)" << std::endl; }

// 全特化 ,选择 f(int,Tp2) 为主模板
template<> 
void f(int,int) { std::cout << "f(int,int)" << std::endl;}

int main()
{
    f('a','c'); // 调用 f(Tp1,Tp2)
    f(1, 2);    // 调用 f(int,int)
    f(1, 'c');  // 调用 f(int,Tp2)

    return 0;
}
/*
输出结果:
f(Tp1,Tp2)
f(int,int)
f(int,Tp2)
*/

函数模板可以在任意位置给默认参数,这与函数参数列表的默认值不同,也与类模板的默认参数不同,下文会介绍类模板的默认参数。

template<class T1 = int,class T2>
void f(){}

类模板

类是支持全特化和偏特化的。

// 主模板类
template<class Tp,int sz>
class A{ };

// 偏特化
template<int sz>
class A<char,sz> { };

// 偏特化
template<class Tp,int sz>
class A<Tp*,sz> { };

// 全特化
template<>
class A<double,20> { };

类模板的模板参数,一旦有一个是默认参数,则其后的参数都需要有默认值。并且要注意的的是偏特化的模板类中是不能提供默认模板参数的

// 偏特化
template<class Tp,int sz = 10>  // 报错:部分专用化不能带有默认模板参数
class A<Tp*,sz> { };

并且偏特化的类会继承主模板的默认模板参数:

// 主模板类
template<class Tp,int sz = 10>
class A{ };
// 偏特化
template<int sz>
class A<char,sz>    // sz 的默认值依然是 10
{ 
public:
    A()
    { std::cout << sz << std::endl; }
};

int main()
{
    A<char> a;  
    return 0;
}
/*
输出结果:
10
*/

非类型模板参数

从上文的例子中也可以看到,模板参数不一定是类型,也可以是类似函数参数的参数,当然也可以有默认值:

template<int a,long long b = 1000>
void f() { }

但并不是所有的类型都可以作为模板参数,根据 C++ 标准,非类型模板参数可以是以下几种类型:

  • 整型常量(例如 int, char, bool 等)

  • 枚举类型

  • 指向对象或函数的指针

  • 引用类型

  • 指向成员的指针

  • std::nullptr_t 类型的值

从 C++17 开始,支持将非类型模板参数设置为 auto 这样的自动类型:

template<int a,auto b>
void f() { return b; }

template<int a,decltype(auto) b>
void f(){ }

要注意的是指定非类型模板参数的值时,只能指定编译期就能求值的表达式,否则会报错,这时因为模板的参数要在编译时就确定,才能让编译器去生成对应的函数实例。

模板的分文件编写

对于一般的函数我可能会将其进行定义和声明的分离编写,即声明写道 .h 文件中,定义写到 .cpp 文件中,但是对模板来说直接这样写会有一些问题。

// Example.h
// 函数声明
template<class T>
void func(T);
// Example.cpp
// 函数定义
template<class T>
void func(T)
{
    // 实现
}
// main.cpp
# include<Example.h>
int main()
{
    func(1);
    return 0;
}

按照一般的分离编译写法如上。

在编译时,在调用模板的地方会在 main.cpp 文件中找对应的函数地址,但是只能找到声明,没有定义,没办法进行函数实例化,就得不到对应的地址,所以只能等到链接的时候到其他文件或库中找该函数的地址。

但是到链接的过程时,却根本找不到对应的函数实现,应为对应模板根本没有实例化,所以就会报链接错误。

解决方法有两种:

  • 第一种就是在 .cpp 文件中进行显示实例化
// Example.cpp
template<class T>
void func(T)
{
    // 实现
}
// 显示实例化
template void func(int);

但是这种方法缺陷也很明显,会让代码编写变得非常麻烦,每次需要新的类型都要加上新的显示实例化。

  • 第二种就是将模板的定义和声明写道同一个文件中(.h 或 .hpp 文件中),就从根源上解决了该问题,这通常更简单实用。

typenameclass

通常情况下 typenameclass 用作 template<> 中声明模板参数的。但是在访问一些还未实例化的类中定义的类型时,也要用到 typenameclass

下面的例子中 list 是自己实现的链表,在其中定义了一个 const_iterator 的迭代器类型:

template<class T>
void print(const myLib::list<T>& a)
{
    typename myLib::list<T>::const_iterator it = a.begin();
    while(it != a.end())
    {
        cout << *it << ' ';
        it++;
    }
    cout << endl;
}

当模板还未进行实例化之前,编译器是不清楚 myLib::list<T>::const_iterator 这是一个类型还是 myLib::list<T> 下的一个静态成员变量,所以会发生报错,只有加上 typenameclass 告诉编译器后面这些是一个变量才能正确编译通过。

模板基类

如果一个类继承自一个模板基类,那么对它即使 x 是继承而来的,使用 this->x 和直接使用 x 也不一定是等效的。比如:

template<class>
class Base
{
public:
    void bar(){ }
};

template<class Tp>
class Derived : public Base<Tp>
{
public:
    void foo() {
        bar(); // 调用外部函数
    }
};

这里 bar() 不会被解析为 Base 中的 bar(),这样要么报错找不到 bar() ,要么调用一个全局的 bar() 导致一些意料之外的错误。要调用 Base 中的 bar() 就要用 this->x 或者 Base<Tp>:: 来调用它。