抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

C++模版

《C++ Templates: The Complete Guide》笔记

为什么使用模版

为了实现泛型

如果不使用泛型,对于一些通用的函数或容器,比如print、vector,我们可能需要逐个实现他们的函数,尽管这些函数的功能十分类似(可能相同),低效复杂而且难以维护

在C++标准库中使用了大量模版

模板的缺点

  • 难以阅读、调试
  • 容易触发编译报错(比如将<<运算符识别为模板展开)
  • 闭源软件不友好(许多开源软件会提供.h.lib,如果使用模板,头文件中就会暴露实现)

函数模版

模版定义

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

int main() {
std::cout << getMax(5, 6) << std::endl; // 6
std::cout << getMax(5.1, 6.1) << std::endl; // 6.1
return 0;
}

这里T类型参数,我们可以使用任意字符,不过习惯上使用T

处于历史原因,我们也可以使用class来代替typename

template<class T>
T getMax(T a, T b){
return a > b ? a : b;
}

模版实例化

在编译阶段,模版会被编译为多个独立的实体,比如我们调用getMax(5, 6),编译器会编译出:

int getMax(int a, int b){
return a > b ? a : b;
}

像这样用具体类型int取代类型参数T的过程叫做实例化

两阶段编译检查

模版分为两步编译:模版定义阶段、模版实例化阶段

在模版定义阶段,不会对类型参数做检查,比如你让两个T对象进行大小比较(a > b),编译器不会去质疑T对象是否支持比较操作符

在模版实例化阶段,模版会再次被检查(尤其是类型参数),如果编译器发现T对象不支持比较操作符,就会报错

我们发现,当我们在实例化一个模版时,编译器需要看到模版的完整定义,但C++函数采用声明和实现分离的思想,函数在编译阶段只需要声明,这出现了冲突。简单的做法是将模版的实现写在头文件中

类型推断

我们在调用getMax时,会根据传入的参数类型,自动推断出T的类型,但这种推断是有限制的

getMax(4, 5);		// OK, T is int
getMax(4.1, 5.1); // OK, T is double
getMax(4, 5.1); // Error, 无法正确推断T的类型

const int a = 1;
getMax(2, a); // OK, T is int, a的const会被decay掉
int b = 2;
int &c = b;
getMax(2, c); // OK, T is int, c的引用会被decay掉

decay

退化(decay)是指数组变指针函数变指针左值变右值的过程

void foo(int arr[]){...}
int main(){
int myArray[5] = {1, 2, 3, 4, 5};
foo(myArray); // 等价于foo(&myArray[0])
}

在上面这段代码,数组myArray被转化为指向数组第一个元素的指针

void foo(){...}
int main(){
void (*ptr)() = foo;
ptr();
}

在上面这段代码,函数foo被转化为函数指针

const int a = 1;
int b = 2;
int &c = b;
getMax(2, a); // OK, T is int, a的const会被decay掉
getMax(2, c); // OK, T is int, c的引用会被decay掉

在上面这段代码,int&const int&都被转化为对应的右值

我们可以使用type_traits库来检测decay的合法性

template<typename T, typename U>
struct decay_equiv: std::is_same<typename std::decay<T>::type, U>::type {};

int main(){
std::cout << std::boolalpha << decay_equiv<int&, int>::value << std::endl; // true
std::cout << std::boolalpha << decay_equiv<const int&, int>::value << std::endl; // true
}

std::boolalpha的作用是将bool类型以true/false的形式打印出来,而非1/0

多个模板参数

当模板有多个参数时,我们可以用<>指定参数类型

在后面的类型,如果可以自动推断出来,可以不写

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

int main() {
std::cout << getMax<int, double, int>(4, 7.2) << std::endl; // 7
std::cout << getMax<int>(4, 7.2) << std::endl; // 7
}

返回类型推断

极其推荐这样写

上面的返回值类型是手动指定的,很容易出现编写错误,因此我们一般会让编译器来角色返回值类型

template<typename T1, typename T2>
auto getMax(T1 a, T2 b) -> decltype(a > b ? a : b){
return a > b ? a : b;
}
int main() {
std::cout << getMax(4, 7.2) << std::endl; // 7.2
std::cout << getMax(8, 7.2) << std::endl; // 8
}

我们发现这样写的返回类型由?:运算符的执行结果决定,7.2大于4,于是返回类型为double,8大于7.2,于是返回类型为int

进一步的,若传入的数据是引用类型,可以使用类型萃取,不过一般情况下,上面这种写法就够了

template<typename T1, typename T2>
auto getMax2(T1 a, T2 b) -> typename std::decay< decltype(a > b ? a : b)>::type{
return a > b ? a : b;
}

公共类型

Common Type

C++11提供了一种更一般的类型,用于得到两个模板参数的公共类型

template<typename T1, typename T2>
std::common_type_t<T1, T2> getMax3(T1 a, T2 b){
return a > b ? a : b;
}

默认参数类型

我们可以给参数指定默认值,但要同时给T一个对应默认参数

template<typename T = double>
void foo(T a = 3.14){
std::cout << a << std::endl;
}

int main() {
foo(15); // 15
foo(); // 3.14
}

重载

函数模板可以与同名的普通函数共存,且优先调用普通函数

template<typename T>
void foo(T a){
std::cout << a << std::endl;
}

void foo(int a){
std::cout << "int: " << a << std::endl;
}

int main() {
foo(1); // int: 1
foo(3.14); // 3.14
}

函数模板也可以和其他同名但参数类型数量不同的函数模板共存,但必须保证在调用模板时,有且仅有一个模板能匹配

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

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

注意事项

使用值传递

一般而言,函数参数中,简单类型值传递,复杂的类型引用传递。但在模板编程中,我们更倾向于使用值传递

值传递的优点:

  • 语法简单
  • 编译器能更好地优化
  • 移动一般比拷贝成本更低
  • 某些情况没有移动或拷贝
  • 模板既可以适用于复杂类型,也可以适用于简单类型,盲目使用引用会影响简单类型的使用
  • 调用者可以主动使用std::ref()std::cref()
  • string literal和raw array使用引用传递会出现问题

类模板

类也可以使用模板,STL中的容器就是这样实现的

类模版中的模版成员函数,只有在被调用时才会实例化

类模版的模版参数,要能支持模版函数中使用的各种操作和运算符

template<typename T>
class Stack{
private:
std::vector<T> elems;
public:
void push(T const& elem);
void pop();
T const& top() const;
bool empty() const { return elems.empty(); }
};

template<typename T>
void Stack<T>::push(const T &elem) {
elems.push_back(elem);
}

template<typename T>
void Stack<T>::pop() {
elems.pop_back();
}

template<typename T>
T const& Stack<T>::top() const {
assert(!elems.empty()); // 断言语句,若条件不满足,程序会终止,仅在Debug模式生效
return elems.back();
}

由于历史原因,C++11以前的模版,两个相邻尖括号间要有空格,如Stack<Stack<int> >,在C++11之后就不再需要了

静态成员

template<typename T>
class Foo{
public:
static int count;
};
template<typename T>
int Foo<T>::count = 0;

int main() {
Foo<int>::count = 10;
Foo<double>::count = 20;
std::cout << Foo<int>::count << std::endl; // 10
std::cout << Foo<double>::count << std::endl; // 20
}

特例化

对于特定的类型,我们可以进行特例化,可以做针对性的优化

template<>
class Stack<std::string> {
...
};

特例化也可以部分特例化

template<typename T>
class Stack<T*> {
...
};

可以多模版参数特例化,可以给予默认模版参数

类型别名

类型别名只是为已经存在的类型定义一个别名,并没有创建新类型,可以用typedefusing实现

typedef-name:

typedef Stack<int> IntStack;

alias declaration

using IntStack2 = Stack<int>;

alias declaration也可以被模版化,被称为alias templates

template<typename T>
using MyStack = Stack<T>;

非类型模版参数

所谓的非类型模版参数,就是不用typename T作为模版参数

下面是使用array实现Stack的代码,用户可以手动指定栈容量,我们使用了std::size_t作为非类型模版参数

template<typename T, std::size_t Maxsize>
class Stack{
private:
std::array<T, Maxsize> elems;
std::size_t elemCount;
public:
Stack();
void push(T const& elem);
void pop();
T const& top() const;
bool empty() const { return elemCount == 0; }
std::size_t size() const { return elemCount; }
};

template<typename T, std::size_t Maxsize>
Stack<T, Maxsize>::Stack(): elemCount(0) {}

template<typename T, std::size_t Maxsize>
void Stack<T, Maxsize>::push(const T &elem) {
assert(elemCount < Maxsize);
elems[elemCount] = elem;
++elemCount;
}

template<typename T, std::size_t Maxsize>
void Stack<T, Maxsize>::pop() {
assert(!elems.empty());
--elemCount;
}

template<typename T, std::size_t Maxsize>
T const& Stack<T, Maxsize>::top() const {
assert(!elems.empty());
return elems[elemCount-1];
}

int main() {
Stack<int, 10> st10;
st10.push(15);
std::cout << st10.top() << std::endl;
}

值得注意的是,Stack<int, 10>Stack<int, 20>是两种不同的类型,由于没有定义隐式或显示的转换规则,我们不能使用一个取代另一个,也不能把一个赋值给另一个

有效类型

非类型模版参数只能是整形、枚举、指向对象/函数/成员的指针、指向对象/函数的左值引用、nullptr

template<double VAT>		//!!!, 这个是错误的,因为double不能作为非类型模版参数
double process(double v){
return v * VAT;
}

若表达式中使用了>,要用()包裹起来

template<int I, bool B>
class C;

int main(){
C<42, sizeof (int > 4)> c; // ERROR! 被截断为C<42, sizeof (int >了
C<42, (sizeof (int) > 4)> c; // OK
}

auto

template<typename T, auto Maxsize>
class Stack{
private:
std::array<T, Maxsize> elems;
std::size_t elemCount;
public:
using size_type = decltype(Maxsize); // !!!, 用于推断Maxsize类型
Stack();
void push(T const& elem);
void pop();
T const& top() const;
bool empty() const { return elemCount == 0; }
size_type size() const { return elemCount; }
};

变参模版

就是接受一组数量可变的参数

template<typename T>
void print(T arg){
std::cout << arg << " ";
}

template<typename T, typename... Types>
void print(T first, Types... args){
print(first);
print(args...);
}

template<typename C, typename... Index>
void printElems(C const& coll, Index... id){
print(coll[id]...);
}

int main() {
print("hello", "world", 1314); // hello world 1314
int a[] = {1, 2, 3, 4, 5, 6, 7};
printElems(a, 2, 3); // 3 4
}

折叠表达式

几乎所有二元运算符都可以用于折叠表达式

template<typename ...T>
auto getSum(T...s){
return (...+s); // 等同于((s1 + s2) + s3)...
}

int main() {
std::cout << getSum(1, 2, 3, 4, 5, 6, 7, 8, 9) << std::endl; // 45
}

基础技巧

typename

template<typename ...T>
void foo(T const& coll){
T::const_interator pos; // Error! 会被认为是静态成员
typename T::const_interator pos; // OK! const_interator是定义在T内的类型
}

零初始化

若一个类型没有默认构造函数(比如基础类型和指针类型),被初始化前其值是未定义的,我们可以使用值初始化

template<typename T>
void foo1(){
T x {}; // 零初始化
std::cout << x << std::endl;
}

template<typename T>
void foo2(){
T x;
std::cout << x << std::endl;
}

int main() {
foo2<int>(); // 1, 为啥这是1啊, 因为未定义所以错了
foo2<int>(); // 1
foo1<int>(); // 0, 通过零初始化所以对了
foo2<int>(); // 0, 很难绷, 为啥你又变成0了?未定义果然不靠谱
}

我们可以在默认构造函数中使用零初始化

class Foo{
private:
T x;
public:
Foo() : x{} {
}
void print(){
std::cout << x;
}
};

int main() {
Foo<int> x;
x.print(); // 0
}

如果是非静态成员,也可以这样写

template<typename T>
class Foo{
private:
T x {};
public:
void print(){
std::cout << x;
}
};

this

对于类模版,若基类也依赖于模版参数,那么子类在调用基类的成员时要使用this->Base<T>::修饰

template<typename T>
class Base{
public:
void print(){
std::cout << "Hello world";
}
};

template<typename T>
class Foo: Base<T> {
public:
void foo(){
this->print(); // !这里要使用this->不然调用不到Base的成员
}
};

int main() {
Foo<int> f;
f.foo(); // Hello world
}

成员模版

类的成员也可以是模版,而且模版参数可以不同

template<typename T>
class Stack{
private:
std::deque<T> elems;
public:
void push(T const&);
void pop();
T const& top() const;
bool empty() const {
return elems.empty();
}
template<typename T2>
Stack& operator= (Stack<T2> const&);

template<typename > friend class Stack; // 为了访问其他Stack实例的私有成员
};

template<typename T>
template<typename T2>
Stack<T>& Stack<T>::operator=(const Stack<T2> &op2) {
elems.clear();
elems.insert(elems.begin(), op2.elems.begin(), op2.elems.end());
return *this;
}

int main() {
Stack<float> floatStack;
for(int i = 0; i < 5; i++){
float n = 3.14 * i;
floatStack.push(n);
std::cout << n << " "; // 0 3.14 6.28 9.42 12.56
}
std::cout << std::endl;
Stack<int> intStack;
intStack = floatStack;
while (!intStack.empty()){
std::cout << intStack.top() << " "; // 12 9 6 3 0
intStack.pop();
}
}

lambda

lambda表达式本质上是成员模版的简化

[](auto x, auto y){
return x + y;
}

变量模版

变量也可以使用模版,必须指定类型

template<typename T = long double>
constexpr T pi {3.1415926535897932385};

int main() {
std::cout << pi<float> << std::endl;
std::cout << pi<double> << std::endl;
std::cout << pi<> << std::endl;
}

C++17中类型萃取就是使用了变量模版

namespace std{
template<typename T>
constexpr bool is_const_v = is_const<T>::value;
}

模版参数模版

模版参数也可以是一个类模版

template<typename T, template<typename Elem> class Cont = std::deque>
class Stack{
private:
Cont<T> elems;
public:
...
};

移动语义

移动的本质是所有权的转移,这里不过多赘述

移动(move):将原对象拷贝或赋值给目标对象时,若原对象马上要被销毁,可以将原对象对内部资源和状态的所有权直接转移给目标对象,避免了非必要的拷贝和临时对象

禁用函数模版

C++11提供了辅助模版std::enable_if<>,可以在编译期间忽略掉一些函数模版

注意看,foo()前面的返回值类型是由std::enable_if决定的,

template<typename T>
typename std::enable_if<(sizeof (T) > 4)>::type
foo(){
std::cout << sizeof (T) << std::endl;
}

int main() {
foo<int>(); // Error! 没有找到foo, int size 4
foo<double>(); // OK, double size 8
foo<long long>(); // OK, long long size 8
}

(sizeof (T) > 4)为false,根据模版中SFINAE(substitute failure is not an error)规则,替换失败不是错误,而是会将这个函数模版忽略掉,于是foo<int>()会报找不到函数的错

(sizeof (T) > 4)为true,std::enable_if<>会被拓展为void,如果你给了第二个参数Tstd::enable_if<>会被拓展为T类型

template<typename T>
std::enable_if_t<(sizeof (T) > 4), T>
foo(){
std::cout << sizeof (T) << std::endl;
}

更明智的写法是

template<typename T>
using EnableIfSizeGreater4 = std::enable_if_t<(sizeof (T) > 4)>;

template<typename T, typename = EnableIfSizeGreater4<T>>
void foo(){
std::cout << sizeof (T) << std::endl;
}

禁用构造函数模版

由于转移语义的存在,构造函数的参数类型很可能是错误的,比如传入了一个右值引用

Person(std::string&& s);		// 我们希望禁用这个构造函数

下面是一个例子

template<typename T>
using EnableIfString = std::enable_if_t<std::is_convertible_v<T, std::string>>;

class Person{
private:
std::string name;
public:
template<typename STR, typename = EnableIfString<STR>>
explicit Person(STR&& n) : name(std::forward<STR>(n)) {
std::cout << "模版构造函数 " << name << std::endl; // 使用了完美转发
}
Person(Person const& p) : name(p.name) {
std::cout << "拷贝构造函数 " << name << std::endl;
}
Person(Person&& p) : name(std::move(p.name)) {
std::cout << "移动构造函数 " << name << std::endl;
}
};

int main() {
std::string s = "sname";
Person p1(s); // 模版构造函数 sname
Person p2("temp"); // 模版构造函数 temp
Person p3(p1); // 拷贝构造函数 sname
Person p4(std::move(p1)); // 移动构造函数 sname
}

我们没法禁用拷贝和移动构造函数,因为我们用成员函数来替代这些函数时,这些函数还会生成默认构造函数

concept

enable_if的语法非常丑,而且还额外使用了一个模版参数,使得代码不易读懂

我们需要的是一个能对函数施加限制的语言特性,当限制不满足时函数会被忽略掉,于是C++决定引入concept

模板元编程

模板在编译器实例化,在实例化时可以进行简单的计算,这就是模板元编程

下面是用模板元判断一个数是不是质数

template <unsigned int N, unsigned int Divisor>
struct IsPrime {
static constexpr bool value = (N % Divisor != 0) && IsPrime<N, Divisor - 1>::value;
};

template <unsigned int N>
struct IsPrime<N, 2> {
static constexpr bool value = (N % 2 != 0);
};

template <unsigned int N>
constexpr bool isPrime = IsPrime<N, N / 2>::value;

int main() {

constexpr unsigned int number = 17;
if (isPrime<number>) {
std::cout << number << " is a prime number." << std::endl;
} else {
std::cout << number << " is not a prime number." << std::endl;
}
}

decltype

选择合适的模板特化,也叫SFINAE

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}

int main() {
std::cout << add(1, 2) << std::endl; // int
std::cout << add(1, 1.5) << std::endl; // double
std::cout << add(1, 1.5f) << std::endl; // float
return 0;
}

if constexpr

编译器if

template <typename T>
float addIfFloat(T t) {
if constexpr (std::is_integral_v<T>) {
return t;
}
return t + 1.0f;
}

int main() {
std::cout << addIfFloat(1) << std::endl; // 1
std::cout << addIfFloat(1.5f) << std::endl; // 2.5
}

类型萃取

感觉不如C#的反射

用于编译期获得模板参数的性质,下面是判断一个类中是否有成员函数Execute()的示例

template <typename T>
struct hasFunction_Execute {
private:
template <typename U>
static auto check(U* ptr) -> decltype(std::declval<U>().Execute(), std::true_type{});

template <typename U>
static std::false_type check(...);

public:
static constexpr bool value = decltype(check<T>(nullptr))::value;
};
struct Check {
void Execute() {
std::cout << "Check::someFunction() called." << std::endl;
}
};

struct NoCheck {

};

int main() {
if (hasFunction_Execute<Check>::value) {
Check obj;
obj.Execute();
}

if (hasFunction_Execute<NoCheck>::value) {
// 不会执行到这里,因为NoCheck类没有Execute成员函数
}
return 0;
}

注意这里使用了std::declval,可以用作引用占位符

评论