C++ 模板01--模板基础

包括函数模板, 类模板, 别名模板, 变量模板基本的用法, 偏特化和全特化的基本知识, 以及类型推导的基本用法.

基本用法

函数模板

对于常用的 max 函数, 其返回输入参数中最大的那个. 这一操作对许多种类型都适用, 如果不想对每种参数都实现相同的行为, 就需要使用模板技术定义一个函数模板:

1
2
3
4
5
// return the max value of the args
template<typename T>
T max(T a, T b) {
    return a < b ? b : a;
}

对于上述定义, 所有支持 < 运算符的类型 T 都可以实例化一个 max 函数的特殊版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<string>
int main () {
    int i = 42;
    ::max(7, i); // int max(int ,int ); 42

    double f1 = 3.4;
    double f2 = -6.7;

    ::max(f1, f2); // double max(double, double); 3.4

    std::string s1 = "abc";
    std::string s2 = "abcdefg";
    ::max(s1, s2); // std::string max(std::string, std::string); abcdefg
}

模板也可以有多个不同的参数, 例如:

1
2
3
4
template<typename T1, typename T2, typename RT>
RT max(T1 a, T2 b){
    return a < b ? b : a;
}

此例中可以为函数的参数和返回值分别设置一个类型, 例如:

1
::max<int, double, double>(1, 1.1);

重载

函数模板可以被重载, 即, 另外定义一个非模板函数或不同的函数模板. 当调用是的参数类型完美匹配时, 重载决议会优先决议非模板的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int max(int a, int b) {
    return a < b ? b : a;
}

template<typename T>
T max(T a, T b) {
    return a < b ? b : a;
}

template<typename RT, typename T1, typename T2>
RT max(T1 a, T2 b) {
return a < b ? b : a;
}

int main() {
    ::max(1, 2);   // non-template
    ::max<>(1, 2); // template; int max<int>(int, int);
    ::max(1, 2.1); // non-template;
    ::max<float>(1, 2.1); // template; float max<float, int, float>(int, float);
}

类模板

类型的定义也可以使用一个或多个类型参数化, 例如标准模板库中的容器类型就是类模板.

作为一个例子, 下面定义一个简单的容器适配器 Stack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <vector>
#include <cassert>
template<typename T, typename Cont = std::vector<T>>
class Stack {
private:
    Cont elems; // elements
public:
    void push(T const& elem); // push element
    void pop(); // pop element
    T const& top() const; // return top element
    bool empty() const { // return whether the stack is empty
        return elems.empty();
    }
};

template<typename T, typename Cont>
void Stack<T,Cont>::push (T const& elem) {
    elems.push_back(elem); // append copy of passed elem
}

template<typename T, typename Cont>
void Stack<T,Cont>::pop () {
    assert(!elems.empty());
    elems.pop_back(); // remove last element
}

template<typename T, typename Cont>
T const& Stack<T,Cont>::top () const {
    assert(!elems.empty());
    return elems.back(); // return copy of last element
}

别名模板

C++ 中的 using 关键字也可以模板化:

1
2
3
4
template<typename T>
using DequeStack = Stack<T, std::deque<T>>;

auto stack = DequeStack<int>; // a stack of int with the base container being a std::deque<int>

别名模板还可以用来定义模板化的成员类型, 例如对于迭代器类型, 假如存在定义:

1
2
3
4
5
template<typename T, typename Cont = std::vector<int>>
class Stack {
public:
    using iterator = ...;
}

则可以通过:

1
2
template<typename T>
using StackIterator = typename Stack<T>::iterator;

来定义任意元素类型的 Stack 迭代器类型. 注意 Stack<T>::iterator 前面的 typename 不可缺少, 否则编译器并不敢假设后面的东西是个类型.

C++14 中 type traits 的 _t 后缀

标准模板库中有一种 type traits 基础设施用于对类型进行操作, 其结果也是一种类型. 在 C14 之前, 这类设施只能通过 std::some_traits<T>::type 使用. 在 C14 中, 这类设施都使用 _t 后缀进行了定义:

1
2
3
4
namespace std {
    template<typename T>
    using some_traits_t = typename some_trait<T>::type;
}

这样就可以通过简单直观的方式使用这类设施:

1
std::some_traits_t<T>

变量模板

自 C++14, 变量也可以被参数化定义, 例如:

1
2
3
4
5
6
template<typename T = long double>
constexpr T pi{3.1415926};

std::cout << pi<> << std::endl; // outputs a long double
std::cout << pi<float> << std::endl; // outputs a float
std::cout << pi << std::endl;; // Error, you always need to specify the angle brackets

模板参数也可以使用非类型参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
#include<array>

template<int N>
std::array<int, N> arr{};

template<auto N>
constexpr decltype(N) dval = N;

int main() {
    std::cout << dval<'a'> << std::endl;
    arr<10>[0] = 1;
    for (auto i = 0; i < arr<10>.size(); ++i){
        cout << arr<10>[i] << std::endl;
    }
}

变量模板的一大用处是可以用来定义类的数据成员. 例如, 对于标准模板库中的另一种 type traits 基础设施:

1
2
3
4
5
6
7
8
9
namespace std {
    template<typename T>
    class numeric_limits {
    public:
        ...
        static constexpr bool is_signed = false;
        ...
    };
}

对于类型 T, 只能通过以下方式使用该设施:

1
std::numeric_limits<T>::is_signed

如果定义:

1
2
template<typename T>
constexpr bool isSigned = std::numeric_limits<T>::is_signed;

就可以通过 isSigned<T> 使用该设施。

C++17 中 type taits 的 _v 后缀

基于变量模板技术, C++17 为结果为值的 type traits 基础设施定义了 _v 后缀的版本:

1
2
3
4
namespace std {
    template<typename T>
    constexpr bool is_some_traits_v = is_some_traits<T>::value;
}

偏特化和全特化

特化 (Specialization) 是针对类模板的一种用法, 与函数模板可以重载类似, 类模板的特化提供了一种为特殊的模板参数或模板参数组合实现不同行为的类模板定义方法.
通常类模板的特化可以为特殊的模板参数定义更优化的类实现, 或者修正在某些模板参数下模型的定义有误的情况. 例如, 可以针对 POD 类型提供特殊的容器定义以实现更加紧凑的内存布局和更加快速的数据拷贝.

需要注意的是, 在类模板的特化中, 其所有的成员都需要有对应的特化实现.

全特化 (Specialization)

类模板的全特化声明由一个空的模板参数列表开头:

1
2
template<>
class Stack<std::string>;

在全特化模板的实现中, 所有的成员签名类似于非类模板成员的签名, 即不以 template<> 开头:

1
void Stack<std::string>::push(std::string const& s);

以上就是类模板全特化的基本用法. 在实际使用中, 可以通过全特化为特定类型提供特定的实现. 例如在一个例子中, 可以为 std::string 类型的 Stack 提供一个以 std::deque 为底层容器的版本:

1
2
3
4
5
6
7
8
9
10
11
12
#include<deque>
#include<string>
template<>
class Stack<std::string> {
private:
std::deque<std::string> elems;
public:
void push(std::string const&);
void pop();
std::string const& top() const;
bool empty() const;
}

偏特化 (Partial Specialization)

偏特化主要用在多模板参数的类模板中, 可以利用偏特化技术为多个模板参数中的部分提供具体的特化. 在单参数模板中也可以将类型参数特华为特殊的形式 (如指针类型)

将模板参数特化为特殊形式

1
2
3
4
template<typename T>
class Stack<T*>; // specialization for raw pointers
template<typename T1, typename T2>
class Widget<T1*, T2*>; // specialization for raw pointers

特化部分模板参数

1
2
3
4
5
6
template<typename T1>
class Widget<T1, std::string>; // specialization for std::string as T2
template<typename T1>
class Widget<T1, T1>; // specialization for T1 as T2
template<typename T1, typename T2>
class Widget<T1*, T2*>;

类型推导 (Deduction)

C17 允许对类型模板的实例化语句省略显示的模板类型, C 编译器会自动推导实例化的模板参数.

例如:

1
2
3
Stack<int> istack; // stack of ints
Stack<int> istack2 = istack; // stack of ints
Stack istack3 = istack; // OK since C++17, stack of ints

istack3 的定义语句中不需要显示声明模板参数 <int>, 编译器会自动根据拷贝对象的类型推导出模板参数为 int

这种级别的类型推导是由构造函数的定义决定的, 因此, 只要构造函数提供相应的支持, 类似上面示例中的参数推导可以推广到任意的形式:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
class Stack {
private:
vector<T> elems; // vector as base container
public:
Stack() = default;
Stack(T const& elem)
: elems({elem}) // {} around to initialize vector elems, otherwise wrong constructor will be invoked
{}; // support for construction from one element of type T
};
Stack s = 0; // stack of int, with one element of value 0

对字符串字面量的处理

上面的 Stack 定义支持从任意单个值实例化模板并初始化容器, 并且不需要显示传递模板参数. 但是当以字符串字面量实例化上述模板时, 由于其特殊的类型, 会造成一些特殊结果:

1
Stack s = "string literal"; // "string literal" is of type `char const[15]`, thus s is of type Stack<char const[15]>

这是由于在上面的构造函数定义中, elem 被声明为了引用类型, 而对于字符串字面量的引用类型, 其不会像值类型一样在参数传递的过程中 decay, 因此参数 T 就如实地保留了字符数组的原始类型

如果上面对 Stack 的定义中构造函数定义为参数按值传递, s 的推导类型就会变成 Stack<char const*>.

Deduction Guide

上面对于字符串字面量的处理最多可以将其引导为指针类型, 而且需要改变构造函数的定义. 通过后置类型声明的语法, 则可以为构造函数显示声明实例化的结果.

例如添加以下声明:

1
Stack(char const*) -> Stack<std::string>;

字符串字面量作为初始化物时得到的对象类型就会是 Stack<std::string>.

但需要注意此时无法再使用拷贝操作符初始化 Stack, 这是因为:

1
Stack s = "string literal";

s 实例化为 Stack<std::string>, 那么 s 的定义中构造函数就变为:

1
Stack<std::string>::Stack(std::string const&);

此构造函数无法接纳字符串字面量为参数. 也就是说, 此时拷贝操作符所在的语句对类模板的实例化是成功的, 但调用构造函数的过程失败了.

此失败是由于并不存在从字符串字面量类型即 char const[]std::string 的直接转换. 因此通过以下语句可以实现需要的实例化和初始化:

1
Stack s{"string literal"};

这是因为大括号初始化语句会自动以传入的量为参数调用目标类型的构造函数, 而 std::string 显然是存在以字符串字面量为参数的构造函数的.