模板与泛型
约 14134 字大约 47 分钟
2025-04-06
基本概念
它是一种泛化的编程方式,其实现原理为程序员编写一个函数/类的代码示例,让编译器去填补出不同的函数实现。允许您延迟编写类或方法中的编程元素的数据类型的规范,直到实际在程序中使用它的时候。换句话说,泛型允许您编写一个可以与任何数据类型一起工作的类或方法。
模板是泛型(泛型generic type
——通用类型之意)编程的基础,是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。每个容器都有一个单一的定义,比如向量,我们可以定义许多不同类型的向量,比如 vector
。
模板是一种对类型进行参数化的工具,可以是如下形式:
- **函数模板:**
- **类模板:**
- **别名模板(C++11):**
- **变量模板(C++14):**
- **约束与概念(C++20):**
假如设计一个两个参数的函数,用来求两个对象的和,在实践中我们可能需要定义n多个函数
int add(int a,int b){return a+b;}
char add(char a,char b){return a+b;}
float add(float a,float b){return a+b;}
...
这个例子就是实现了多种不同类型的两个数之间相加,我们这里只是实现了三种,如果要实现很多种,那代码量就非常大。如果可以只用一个函数和类来描述,那将会大大减少代码量,并能实现程序代码的复用性,所以这里就要用到模板了。
对于函数功能相同,唯一的区别就是参数类型不同,在这种情况下,不必定义多个函数,只需要在模板中定义一次即可。在调用函数时编译器会根据实参的类型生成对应类的函数实体(调用的不是模板,而是模板实例化出来的实体),从而实现不同的函数功能。
PS:编译器根据实参生成特定的函数的过程叫做模板特化
函数模板
实际上是定义一个可以生成任意类型函数的模板,它所用到的数据的类型均被作为参数:不指定具体类型,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位)。凡是函数体相同的函数都可以用这个模板来代替,在函数调用时根据传入的实参来逆推出真正的类型,从而产生一个针对该类型的实体函数。这个通用函数就称为函数模板。****
定义格式
template<typename Type,...> //注意后面不能加分号
Type funName(Type val)
{
//Code
}
template
:是声明模板的关键字,告诉编译器开始泛型编程。<typename type>
:尖括号中的typename
是定义模板形参的关键字,用来说明后面的模板形参是类型。typename
还可以用class
关键字来代替,但是推荐用typename
,这样更清晰。
template<typename T>
T add(T a,T b)
{
return a+b;
}
int main()
{
cout << add(2, 4) << endl;
return 0;
}
输出结果:6
,当调用add
函数传入int
类型参数时,首先编译器会去找有没有生成过add
版本的实例,如果没有,将会创建一个。然后进行调用,也就是说真正调用的不是函数模板,而是调用根据模板生成的具体的函数(隐式实例化)。
函数模板调用
对于函数模板,有两种调用方式
- 显示类型调用:
add<int>(2,4);
在函数名后面写上<实参类型>,表示参数是int
类型。add<>(2, 4);
<>里也可以不写类型,加上<>表示调用的是函数模板。
- **自动类型推导:**根据参数的类型进行推导,但是两个参数的类型必须一致,否则会报错。
add('a','c');
那么需要传两个类型不一样的参数要怎么做呢?写两个模板参数类型即可,然后返回类型使用auto
自动推导。
template <typename T,typename U>
auto add(T a,U b)
{
return a+b;
}
模板实例化探究
既然调用函数模板,实际上是调用的实例化版本,那么怎么才能知道,是否已经实例化过了呢?
template<typename T>
int getID()
{
static int id = 0;
return id++;
}
int main()
{
cout << getID<int>() << endl; //0
cout << getID<int>() << endl; //1
cout << getID<float>() << endl; //0
cout << getID<double>() << endl; //0
return 0;
}
根据输出结果,第一次调用getID
时,结果为0,说明这是第一次调用getID
版本。第二次调用时,结果为1,说明这个版本已经实例化过了。接下来调用了float
和double
版本,输出结果都为0,说明都是第一次调用。这种由编译器自动实例化的过程,叫做隐式实例化。
考虑一下这种情况,如果想要通过add
函数,实现两个char*
字符串的相加,那么原有的直接a+b
的方式将不可用,必须使用strcat
来进行拼接。
template<typename _Ty>
_Ty add(_Ty a, const _Ty b)
{
return a + b;
}
int main()
{
char str1[20] = "hello ";
char str2[20] = "world";
cout << add(str1,str2 ) << endl;
return 0;
}
直接报错error C2110: “+”: 不能添加两个指针
,所以我们需要特化一个,用strcat
实现相加。
template<>
char* add<char*>(char* a, char* b)
{
return strcat(a, b);
}
使用template<>
声明特化,然后再特化版本的函数名后面写上<特化的类型>即可!运行代码,此时add(str1,str2 )
将会调用特化版本的函数。OK!没有任何问题了。
cout << add("hello ", "world") << endl;
如果,调用这行代码,那么编译器又会报错error C2110: “+”: 不能添加两个指针
,因为此时传递的参数类型为const char*
,而const char*
不能隐式转换到char*
,所以不会调用char*
类型的特化版本。而是让函数模板重新实例化了const char*
的版本,所有就又会报错了。如果我们不想让调用者传递const char*
类型的参数,则可以删除const char*
版本的特化。
template<>
const char* add<const char*>(const char*, const char*) = delete;
删除之后,cout << add("hello ", "world") << endl;
这行代码将直接编译报错:error C2280: “const char *add<const char*>(const char *,const char *)”: 尝试引用已删除的函数
。
cout << add(str1, "world") << endl;
如果第一个参数是char*
第二个参数是cosnt char*
,代码也将编译不过,报错error C2672: “add”: 未找到匹配的重载函数。
这个时候通过特化根本无法解决,因为两个参数的类型不一致,而我们的模板函数必须保证两个参数类型一致,但是可以通过重新设计,解决掉!
template<typename T1,typename T2>
auto add(T1 a, T2 b)
{
return a + b;
}
template<>
auto add<char*,const char*>(char* a, const char* b)
{
return strcat(a,b);
}
int main()
{
char str1[20] = "hello ";
cout << add(str1, "wrold") << endl;
return 0;
}
将模板中的所有参数确定化,叫做全特化。而针对模板参数进一步进行条件限制的特化,叫做偏特化,而函数模板不支持偏特化,类模板才支持偏特化。
PS:只有模板参数在两个及以上时,才有偏特化
函数模板和普通函数
函数模板:不提供隐式类型转换,严格类型匹配
普通你数:提供隐式类型转换
template<typename T>
void add(T a,T b)
{
cout<<"模板函数"<<a+b<<endl;
}
show('A',65); //“void showSum(T,T)”: 未能从“char”为“T”推导 模板 参数
show<int>('A',65); //显示指定模板类型后,‘A’可以转换到int
所谓的函数模板的重载是指:普通函数版本,函数模板版本和函数模板特例化的版本可以共存,例如:
template<typename T>
void sum(T a, T b)
{
cout << "函数模板" << a + b << endl;
}
template<>
void sum<int>(int a, int b)
{
cout << "函数模板<int>" << a + b << endl;
}
void sum(int a, int c)
{
cout << "普通函数" << a + c << endl;
}
void test()
{
sum(1, 2); //当函数模板和普通函数参数都符合时,优先选择普通函数
sum<>(1, 2); //若显示使用模板函数,则使用<>类型列表
sum(3.0, 4.2); //如果函数模板产生更好的匹配,则使用函数模板
sum('a', 12); //调用普通函数,可以隐式类型转换
}
类模板
除了函数模板外,C++
中还支持类模板。类模板是对成员数据类型不同的类的抽象,它说明了类的定义规则,一个类模板可以生成多种具体的类。与函数模板的定义形式类似, 类模板也是使用template
关键字和尖括号“<>”
中的模板形参进行说明,类的定义形式与普通类相同。
定义格式
template<typename Type,...>
class className
{
//Code
};
- 类模板中的关键字含义与函数模板相同。
- 类模板中的类型参数可用在类声明和类实现中。一旦声明了类模板就可以用类模板的模板参数声明类中的成员变量和成员函数,即在类中使用内置数据类型的地方都可以使用模板形参名来代替。
template<typename _Ty>
class Wrap
{
public:
Wrap() {}
Wrap(_Ty obj) :_obj(obj) {}
friend std::ostream& operator<<(std::ostream& out, const Wrap& other)
{
out << other._obj; //注意:必须确保对象重载了<<(能被输出)
return out;
}
operator _Ty()const
{
return _obj;
}
private:
_Ty _obj{ 0 };
};
单个类模板语法
定义一个类模板非常简单,重要的是如何去用类模板定义对象~
- 在
C++17
之前如果没有指定模板的参数列表,编译器是会报错的
Wrap num(20); //error C2955: “Wrap”: 使用 类 模板 需要 模板 参数列表
- 而在
C++17
以后可以不用指定模板参数列表,可以进行构造函数模板推导(其实我更愿意叫它模板类型自动推导)
Wrap num(20); //C++17及以上标准可以
- 指定参数列表只需要在类模板名的后面加上<类型>即可
Wrap<int> num = 20;
Wrap<std::string> str(string("hello"));
从上面可以看出,类模板只是模板,必须要指定实例化类型,才可以使用。(Wrap
只是模板名,Wrap<int>
才是类名)。
- 类模板不代表一个具体的、实际的类,而代表一类类。实际上,类模板的使用就是将类模板实例化成一个具体的类。
- 只有那些被调用的成员函数,才会产生这些函数的实例化代码。对于类模板,成员函数只有在被使用的时候才会被实例化。显然,这样可以节省空间和时间;
- 如果类模板中含有静态成员,那么用来实例化的每种类型,都会实例化这些静态成员。
template<typename _Ty>
class Vector
{
public:
Vector()
{
_base = new _Ty[_capacity]{ _Ty() };
}
void push_back(const _Ty& val)
{
if (_size >= _capacity)
{
_Ty *p = new _Ty[_capacity+10]{ _Ty() };
std::memcpy(p, _base, _capacity);
_capacity += 10;
delete _base;
_base = p;
}
_base[_size++] = val;
}
_Ty& operator[](int index)
{
return _base[index];
}
size_t size()const
{
return _size;
}
protected:
_Ty* _base{nullptr};
int _size{0};
int _capacity{10};
};
int main()
{
Vector<int> arr;
for (size_t i = 0; i < 10; i++)
{
cout << arr[i] << " ";
}
cout << endl;
Vector<string> string_vec;
string_vec.push_back("hello");
string_vec.push_back("world");
for (size_t i = 0; i < string_vec.size(); i++)
{
cout << string_vec[i] << " ";
}
return 0;
}
继承中的类模板
子类从模板类继承的时候,需要让编译器知道,父类的数据类型具体是什么(数据类型的本质:如何分配内存空间)
class StringList
: public Vector<std::string> //指定具体的类型
{
public:
using Vector::Vector; //using Vector<std::string>::Vector;
//将所有字符串连接起来,使用split分隔
std::string join(const std::string& split = " ")
{
std::string str;
for (int i = 0; i < size()-1; i++)
{
str.append(_base[i]);
str.append(split);
}
str.append(_base[size()-1]);
return str;
}
};
int main()
{
StringList string_vec;
string_vec.push_back("hello");
string_vec.push_back("world");
cout << string_vec.join(",") << endl; //hello,world
return 0;
}
template<typename _Ty>
class List : public Vector<_Ty>
{
public:
using Vector<_Ty>::Vector;
const _Ty* data()const
{
return _base;
}
friend std::ostream& operator<<(std::ostream& out, const List& other)
{
for (int i = 0; i < other.size(); i++)
{
out << other._base[i] << ",";
}
return out;
}
};
代码看起来没有问题,但是在子类中使用父类的成员,会提示找不到标识符。
const _Ty* data()const
{
return this->_base; // error C3861: “_base”: 找不到标识符
}
解决办法:
- 通过`this`指针访问:`this->_base`
- 通过父类访问:`Vector<_Ty>::_base `
为什么会这样?this
有类型Vector<_Ty>
,依赖的类型T
。所以this
有依赖类型。所以需要this->_base
做_base
一个从属名称。
类模板特化
类模板特化分为全特化和偏特化两种
- 将模板中的所有参数确定化,叫做全特化。
- 而针对模板参数进一步进行条件限制的特化,叫做偏特化,而函数模板不支持偏特化,类模板才支持偏特化。
将模板中的所有参数确定化,即所有类型模板参数都用具体类型代表,特化版本模板参数列表为空template<>
,在特化版本的类名后面加上<type,...>
。
template<typename T,typename U>
struct Test
{
void show()
{
cout<<"非特化版本"<<endl;
}
};
//全特化版本
template<>
struct Test<int,int>
{
void show()
{
cout<<"int,int特化版本"<<endl;
}
};
//特化版本可以有任意多个
template<>
struct Test<double,string>
{
void show()
{
cout<<"double,string特化版本"<<endl;
}
};
//测试
int main()
{
Test<int,int> t;
t.show(); //int,int特化版本
Test<double, string> t1;
t1.show(); //double,string特化版本
Test<char, char> t2;
t2.show(); //非特化版本
return 0;
}
- 从模板参数数量上
//从模板参数数量上
template<typename T,typename U>
struct Test
{
void show()
{
cout<<"非特化版本"<<endl;
}
};
//局部特化
template<typename U>
struct Test<int,U>
{
void show()
{
cout<<"非特化版本"<<endl;
}
};
//测试
int main()
{
Test<int,string> tt;
tt.show(); //局部特化版本
return 0;
}
- 从模板参数范围上:`int -> int&`
//从模板参数范围上
template<typename T>
struct Test
{
void show()
{
cout<<"非特化版本"<<endl;
}
};
//const T
template<typename T>
struct Test<T&>
{
void show()
{
cout<<"T&特化版本"<<endl;
}
};
//T*
template<typename T>
struct Test<T*>
{
void show()
{
cout<<"T*特化版本"<<endl;
}
};
//测试
int main()
{
Test<int> t;
t.show(); //非特化版本
Test<int*> t1;
t1.show(); //T*特化版本
Test<int&> t2;
t2.show(); //T&特化版本
return 0;
}
模板声明和实现分离
使用C/C++
进行编程时,一般会使用头文件以使定义和声明分离,并使得程序以模块方式组织。将函数声明、类的定义放在头文件中,而将函数实现以及类成员函数的定义放在独立的文件中。
template<typename _Ty>
class Vector
{
public:
Vector();
Vector(const Vector& other);
void push_back(const _Ty& val);
_Ty& operator[](int index);
size_t size()const;
//友元函数内部声明
template<typename _Ty>
friend std::ostream& operator<<(std::ostream& out, const Vector<_Ty>& other);
protected:
_Ty* _base{ nullptr };
int _size{ 0 };
int _capacity{ 10 };
};
//成员函数外部实现
template<typename _Ty>
Vector<_Ty>::Vector()
{
_base = new _Ty[_capacity]{ _Ty() };
}
template<typename _Ty>
Vector<_Ty>::Vector(const Vector& other);
template<typename _Ty>
void Vector<_Ty>::push_back(const _Ty& val)
{
if (_size >= _capacity)
{
_Ty* p = new _Ty[_capacity + 10]{ _Ty() };
std::memcpy(p, _base, _capacity);
_capacity += 10;
delete _base;
_base = p;
}
_base[_size++] = val;
}
template<typename _Ty>
_Ty& Vector<_Ty>::operator[](int index)
{
return _base[index];
}
template<typename _Ty>
size_t Vector<_Ty>::size()const
{
return _size;
}
//友元函数类外实现
template<typename _Ty>
std::ostream& operator<<(std::ostream& out, const Vector<_Ty>& other)
{
for (int i = 0; i < other.size(); i++)
{
out << other._base[i] << ",";
}
return out;
}
SVector.h
#pragma once
#include<iostream>
template<typename _Ty>
class Vector
{
public:
Vector();
Vector(const Vector& other);
void push_back(const _Ty& val);
_Ty& operator[](int index);
size_t size()const;
//友元函数内部声明
template<typename _Ty>
friend std::ostream& operator<<(std::ostream& out, const Vector<_Ty>& other);
protected:
_Ty* _base{ nullptr };
int _size{ 0 };
int _capacity{ 10 };
};
SVector.cpp
#include "SVector.h"
//成员函数外部实现
template<typename _Ty>
Vector<_Ty>::Vector()
{
_base = new _Ty[_capacity]{ _Ty() };
}
template<typename _Ty>
Vector<_Ty>::Vector(const Vector& other);
template<typename _Ty>
void Vector<_Ty>::push_back(const _Ty& val)
{
if (_size >= _capacity)
{
_Ty* p = new _Ty[_capacity + 10]{ _Ty() };
std::memcpy(p, _base, _capacity);
_capacity += 10;
delete _base;
_base = p;
}
_base[_size++] = val;
}
template<typename _Ty>
_Ty& Vector<_Ty>::operator[](int index)
{
return _base[index];
}
template<typename _Ty>
size_t Vector<_Ty>::size()const
{
return _size;
}
//友元函数类外实现
template<typename _Ty>
std::ostream& operator<<(std::ostream& out, const Vector<_Ty>& other)
{
for (int i = 0; i < other.size(); i++)
{
out << other._base[i] << ",";
}
return out;
}
test_SVector.cpp
#include"SVector.h"
int main()
{
Vector<int> nums;
for (int i = 0; i < 5; i++)
{
nums.push_back(2 + i);
}
std::cout << nums << std::endl;
return 0;
}
运行报错:
error LNK2019: 无法解析的外部符号 "public: __cdecl 【Vector<int>::Vector<int>(void)】" (??0?$Vector@H@@QEAA@XZ),函数 main 中引用了该符号
error LNK2019: 无法解析的外部符号 "public: void __cdecl 【Vector<int>::push_back(int const &)】" (?push_back@?$Vector@H@@QEAAXAEBH@Z),函数 main 中引用了该符号
error LNK2019: 无法解析的外部符号 "class std::basic_ostream<char,struct std::char_traits<char> > & __cdecl 【operator<<<int>】(class std::basic_ostream<char,struct std::char_traits<char> > &,class Vector<int> const &)" (??$?6H@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@std@@AEAV01@AEBV?$Vector@H@@@Z),函数 main 中引用了该符号
报错原因如下:
当源码文件test_SVector.cpp
中涉及到模板函数的调用时,因为模板函数的定义在另一个源码文件SVector.cpp
中,编译器目前仅仅知道它们的声明。所以,在test_SVector.cpp
中调用push_back
函数以及<<
重载函数时,编译器认为这些函数的实现是在其他源码文件中的,编译器不会报错,因为连接器会最终将所有的二进制文件进行连接,从而完成符号查找,形成一个可执行文件。
尽管编译器也编译了包含模板定义的源码文件SVector.cpp
,但是该文件仅仅是模板的定义,而并没有真正的实例化出具体的函数来。因此在链接阶段,编译器进行符号查找时,发现源码文件中的符号,在所有二进制文件中都找不到相关的定义,因此就报错了。
注意全特化版本是一个实例化的函数/类,而不是模板,所以声明必须放在头文件,而实现必须放在源文件。否则会有重定义问题。那么将这个特化放在何处?显然是要放在模板的头文件中。但这样会导致符号多重定义的错误。原因很明显,模板特化是一个函数,而非模板。
//test.h
#pragma once
#include<iostream>
using namespace std;
template<typename T>
int compare(T a, T b)
{
cout << "T" << endl;
return a == b ? 0 : (a > b ? 1 : -1);
}
template<>
int compare(const char* str1, const char* str2)
{
cout << "特化 const char *" << endl;
return strcmp(str1, str2);
}
//maye.cpp
#include"test.h"
//main.cpp
int main()
{
cout << compare("A", "a") << endl;
//compare<char const *>(char const *,char const *)" 已经在 main2.obj 中定义
return 0;
}
没有理由不在头文件中定义函数——但是一旦这样做了,那么便无法在多个文件中#include
该头文件。肯定会有链接错误。怎么办呢?函数模板特化即函数,而非模板的概念,完全与普通函数一样;加上inline
关键字或者分文件实现都是可以的。
template<>
inline int compare(const char* str1, const char* str2)
{
cout << "特化 const char *" << endl;
return strcmp(str1, str2);
}
因为编译器直接扩展内联函数,不产生外部符号,在多个模块中 #include
它们没有什么问题。链接器不会出错,因为不存在多重定义的符号。对于像compare
这样的小函数来说,inline
怎么说都是你想要的(它更快)。但是,如果你的特化函数很长,或出于某种原因,你不想让它成为 inline
,那要如何做呢?声明和实现分开即可
//test.h
template<>
int compare(const char* str1, const char* str2);
//test.cpp
template<>
int compare(const char* str1, const char* str2)
{
cout << "特化 const char *" << endl;
return strcmp(str1, str2);
}
编译模型
使用C/C++
进行编程时,一般会使用头文件以使定义和声明分离,并使得程序以模块方式组织。将函数声明、类的定义放在头文件中,而将函数实现以及类成员函数的定义放在独立的文件中。
编译器并不是把模板编译成一个可以处理任何类型的单一实体;而是如果调用了模板的时候,编译器才产生特定类型的模板实例。一般而言,当调用函数的时候,编译器只需要看到函数的声明。类似地,定义类类型的对象时,类定义必须可用,但成员函数的定义不是必须存在的。因此,应该将类定义和函数声明放在头文件中,而普通函数和类成员函数的定义放在源文件中。
模板则不同:要进行实例化,编译器必须能够访问定义模板的源代码。当调用函数模板或类模板的成员函数的时候,编译器需要函数定义,需要那些通常放在源文件中的代码。标准 C++
为编译模板代码定义了两种模型。分别是包含编译模型和分别编译模型。所谓包含编译模型,说白了,就是将函数模板的定义放在头文件中。因此,对于上面的例子,就是将test.cpp
的内容都放到test.h
中。为了区分,申明和定义放在一起的文件可以取名叫做.hpp
,所以,结论就是,把模板的定义和实现都放到头文件中。
可变参数模板
可变参数模板的概念和语法
如果一个函数需要的参数个数以及参数类型不确定时,我们急需一种能够让参数可变的方法!
- 在参数类型一致,参数个数不同时可以使用
initializer_list
- 在参数类型不一致时,需要使用
C++
提供的可变参数模板
来个代码:
template<typename ...Args>
void foo(Args ...args)
{
std::cout << __FUNCSIG__ << " args count:" << sizeof...(args) << std::endl;
}
在上面的代码中typename...Args
是类型模板形参包,他可以接受零个或者多个类型的模板实参。Args ...args
叫做函数形参包,它出现在函数的形参列表中,可以接受零个或者多个函数实参。sizeof...(args)
其中sizeof...
是C++
的运算符,专门用来获取形参包的参数个数。以上这些语法概念看起来可能会有点复杂。不过没关系,结合下面的例子会发现这个语法实际上非常明了:
int main()
{
foo(); //foo<>;
foo(1); //foo<int>;
foo(1, 'A'); //foo<int,char>;
foo(1, 2,"hello"); //foo<int,int,const char*>;
return 0;
}
以上是一个变参函数模板,它可以接受任意多个实参,编译器会根据实参的类型和个数推导出形参包的内容,然后生成对应的实例化函数。需要注意的是,函数形参包可以与普通形参结合,但是对于结合的顺序有一些特殊要求。对于函数模板而言,模板形参包不必出现在最后,只要保证后续的形参类型能够通过实参推导或者具有默认参数即可,例如:
template<typename ...Args,typename T,typename U = double>
void foo(T t,U u,Args ...args) //Args ...args这里必须放到最后
{}
虽然以上介绍的都是类型模板形参,但是实际上非类型模板形参也可以作为形参包,而且相对于类型形参包,非类型形参包则更加直观:
template<int ...Args>
void bar(){}
int main()
{
bar<1, 2, 3, 4, 5>();
}
形参包展开
虽然上面已经简单介绍了可变参数模板的基本语法,但是大家应该已经注意到,节中的例子并没有实际用途,函数体都是空的。实际上,它们都缺少了一个最关键的环节,那就是形参包展开,简称包展开。只有结合了包展开,才能发挥变参模板的能力。需要注意的是,包展开并不是在所有情况下都能够进行的,允许包展开的场景包括以下几种。
表达式列表
初始化列表
基类描述
成员初始化列表
函数参数列表
模板参数列表
lambda表达式捕获列表
sizeof…运算符
对其运算符
属性列表
虽然这里列出的场景比较多,但是因为大多数是比较常见的场景,所以理解起来应该不会有什么难度。让我们通过几个例子来说明包展开的具体用法:
template<typename T>
T print(T t)
{
cout << t << endl;
return t;
}
template<class ...Args>
void unpack(Args ...args) {}
template<typename ...Args>
void foo(Args ...args)
{
unpack(print(args)...);
}
int main()
{
foo(1, 5.0, 8);
}
在上面的代码中,print
是一个普通的函数模板,它将实参通过std::cout
输出到控制台上。unpack
是一个可变参数的函数模板,不过这个函数什么也不做。在main
函数中调用了foo
函数模板,并传递了参数,在它的函数体里面对形参包进行了展开,其中print(args)...
是包展开,而print(args)
就是模式,也可以理解为包展开的方法。所以这段代码相当于:
void foo(int a1, double a2, int a3)
{
unpack(print(a1), print(a2), print(a3));
}
对于这个代码来说,就非常清晰了,其实unpack
这个空函数,就是用来容纳包展开的内容的,那么是不是也可以通过一个数组做到这个事情呢?
template<typename ...Args>
void foo(Args ...args)
{
//unpack(print(args)...);
auto arr = {(print(args),0)...};
}
这样的话,我们就不需要再写一个unpack
的函数了,更为简洁!!!
可变参数模板的递归
在上面的形参包展开中也能输出所有参数,但是比较麻烦,接下来看一下递归方式输出,比如下面的案例:
template<typename T,typename ...Args>
void foo(T t,Args ...args)
{
cout << t << endl;
}
在这里只能获取到第一个参数,至于args
需要展开才能得到,我们可以用递归的方法实现。
template<typename T,typename ...Args>
void foo(T t,Args ...args)
{
cout << t << endl;
foo(args...,0);
}
这样就可以打印出所有的实参了,但是会发现递归没有停止,最终会爆栈,所以必须想一个办法终止递归!
- [x] **方法一:传入一个结束数据**
template<typename T,typename ...Args>
void foo(T t,Args ...args)
{
if(t == 0)
return;
cout << t << endl;
foo(args...,0);
}
在这里仅仅是加了一个判断,当t == 0
,也是就是foo(args...,0);
这个调用的最后一个参数时,退出递归!当然这个有个坏处,就是当调用者的参数中出现了0
时,递归会提前结束。
- [x] **方法二:使用函数重载**
template<typename T>
void foo(T t)
{
cout << t << endl;
}
template<typename T, typename ...Args>
void foo(T t, Args ...args)
{
cout << t << endl;
foo(args...);
}
foo(1, 2, 3);
在这里首先调用有参数包的foo
函数,先把t
输出,然后递归,此时会把2,3
传给自己,然后t
就为2
,继续调用foo
,此时会发现参数只有一个,值为3
,他就会去调用void foo(T t)
这个版本的函数,然后退出!
在C++11
标准中,要对可变参数模板形参包的包展开进行逐个计算需要用到递归的方法。
template<class T>
T sum(T arg)
{
return arg;
}
template<typename T,typename ...Args>
auto sum(T arg, Args ...args)
{
return arg + sum(args...);
}
int main()
{
cout << sum(1, 2, 3, 4) << endl; //10
}
在上面的代码中,当传入函数模板sum
的实参数等于1是,编译器会选择调用T sum(T arg)
,该函数什么也没做,直接把传入的参数返回。当传入的实参数量大于1是,编译器会选择调用auto sum(T arg, Args ...args)
,注意,这里使用C++14
的特性将auto
作为返回类型的占位符,把返回类型的推导交给编译器。这个函数除了第一个形参之外,其他形参作为递归调用了sum
函数,然后将其结果与第一个形参求和。最终编译器生成的结果应该和下面的伪代码类似:
sum(double arg)
{
return arg;
}
sum(double arg0, double args1)
{
return arg0 + sum(args1);
}
sum(int arg1, double args1, double args2)
{
return arg1 + sum(args1, args2);
}
int main()
{
std::cout << sum(1, 5.0, 11.7) << std::endl;
}
这个的难点和重点在于initializer_list<int>{(printData(args), 0)...};
,这一行代码用到了列表和逗号表达式的特性,不用说列表的每个值最后都被初始化为0,但是列表的每个值被初始化为0的时候,他们会先执行printData(args(n))
,也就是会不断打印,参数包不断展开
template <typename _Ty>
void printData(_Ty data) {
cout << data << "\t";
}
template <typename ...Args>
void printArgs(Args ...args)
{
initializer_list<int>{(printData(args), 0)...};
cout << endl;
}
完美转发一般是用来统一接口,也就是有许多函数,他们的参数数量、类型不同,我们把他们统一为只用函数名就可以调用该函数,且不减少其原功能。这里我们用仿函数接收一下用bind
绑定的函数以及参数包,注意这里函数和参数包绑定的时候都用了完美转发。
什么是完美转发?forword
是为了解决在函数模板中,使用右值引用参数(T&&)
,传递右值进去以后,类型会变为左值的问题。当传入的参数是一个对象时,右值变左值就会出问题,因为左值调用拷贝构造,右值调用移动构造。本来可以用移动构造提高效率,却因为右值变成左值,调用了拷贝构造。所以我们要把它变回去!实参传的是右值,进入函数体还是右值,这就是完美转发。
class Test
{
public:
void printk()
{
if (func) func();
}
template <typename Func,typename ...Args>
void connect(Func&& f, Args&& ...args) //右值引用
{
func = bind(forward<Func>(f), forward<Args>(args)...);
}
protected:
function<void()> func;
};
void sum(int a, int b)
{
cout<< a + b;
}
int main()
{
Test test;
test.connect(sum, 1, 2);
test.printk();
test.connect([](int a, int b) {cout << endl << a + b; }, 3, 8);
test.printk();
return 0;
}
上面的例子中通过connect
绑定函数和参数包,实现统一接口的功能,通过printK
函数调用。
折叠表达式
在前面的例子中,我们提到了利用数组和递归的方式对形参包进行计算的方法。这些都是非常实用的技巧,解决了C++11
标准中包展开方法并不丰富的问题。不过实话实说,递归计算的方式过于烦琐,数组和括号表达式的方法技巧性太强也不是很容易想到。为了用更加正规的方法完成包展开,C++
委员会在**C++17**
标准中引入了折叠表达式的新特性。让我们使用折叠表达式的特性改写递归的例子:
template<typename ...Args>
auto sum(Args ...args)
{
return (args + ...);
}
int main()
{
std::cout << sum(1, 5.0, 11.7) << std::endl;
}
如果你是第一次接触折叠表达式,一定会为以上代码的简洁感到惊叹。在这份代码中,我们不再需要编写多个sum函数,然后通过递归的方式求和。需要做的只是按照折叠表达式的规则折叠形参包(args + ...)
。根据折叠表达式的规则,(args + ...)
会被折叠为arg0 + (arg1 + arg2)
,即1 + (5.0 + 11.7)
。
到此为止,大家应该已经迫不及待地想了解折叠表达式的折叠规则了吧。那么接下来我们就来详细地讨论折叠表达式的折叠规则。在C++17
的标准中有4种折叠规则,分别是一元向左折叠、一元向右折叠、二元向左折叠和二元向右折叠。上面的例子就是一个典型的一元向右折叠:
(args op ...)折叠为(arg0 op (arg1 op ... (argN-1 op argN)))
对于一元向左折叠而言,折叠方向正好相反:
(... op args )折叠为((((arg0 op arg1) op arg2) op ...) op argN)
二元折叠总体上和一元相同,唯一的区别是多了一个初始值,比如二元向右折叠:
(args op ... op init )折叠为(arg0 op (arg1 op ...(argN-1 op (argN op
init)))
二元向左折叠也是只有方向上正好相反:
(init op ... op args )折叠为(((((init op arg0) op arg1) op arg2) op
...) op argN)
虽然没有提前声明以上各部分元素的含义,但是大家也能大概看明白其中的意思。这其中,args
表示的是形参包的名称,init
表示的是初始化值,而op
则代表任意一个二元运算符。值得注意的是,在二元折叠中,两个运算符必须相同。在折叠规则中最重要的一点就是操作数之间的结合顺序。如果在使用折叠表达式的时候不能清楚地区分它们,可能会造成编译失败,例如:
template<typename ...Args>
auto sum(Args ...args)
{
return (args + ...);
}
int main()
{
cout << sum(std::string("hello "), "C++", "Maye") << endl;
}
上面的代码会编译失败,理由很简单,因为折叠表达式(args +…)向右折叠,所以翻译出来的实际代码是(std::string("hello ") + ("c++ " + "Maye"))
。但是两个原生的字符串类型是无法相加的,所以编译一定会报错。要使这段代码通过编译,只需要修改一下折叠表达式即可:
template<typename ...Args>
auto sum(Args ...args)
{
return (... + args);
}
这样翻译出来的代码将是((std::string("hello ") +"c++ ") + "world")
。而std::string
类型的字符串可以使用+将两个字符串连接起来,于是可以顺利地通过编译。最后让我们来看一个有初始化值的例子:
template<typename ...Args>
void print(Args ...args)
{
(std::cout << ... << args) << std::endl;
}
在上面的代码中,print
是一个输出函数,它会将传入的实参输出到控制台上。该函数运用了二元向左折叠(std::cout <<…<<args)
,其中std::cout
是初始化值,编译器会将代码翻译为(((std::cout << std::string("hello ")) << "c++ ")<< "world") << std::endl;
。
一元折叠表达式中空参数包的特殊处理
一元折叠表达式对空参数包展开有一些特殊规则,这是因为编译器很难确定折叠表达式最终的求值类型,比如:
template<typename ...Args>
auto sum(Args ...args)
{
return (args + ...);
}
在上面的代码中,如果函数模板sum
的实参为空,那么表达式args +…
是无法确定求值类型的。当然,二元折叠表达式不会有这种情况,因为它可以指定一个初始化值:
template<typename ...Args>
auto sum(Args ...args)
{
return (args + ... + 0);
}
这样即使参数包为空,表达式的求值结果类型依然可以确定,编译器可以顺利地执行编译。为了解决一元折叠表达式中参数包为空的问题,下面的规则是必须遵守的。
- 只有`&&、||`和`,`运算符能够在空参数包的一元折叠表达式中使用。
- `&&`的求值结果一定为`true`。
- `||`的求值结果一定为`false`。
- `,`的求值结果为`void()`。
- 其他运算符都是非法的。
template<typename ...Args>
auto andop(Args ...args)
{
return (args && ...);
}
int main()
{
std::cout<< std::boolalpha << andop()<<std::endl;
}
在上面的代码中,虽然函数模板andop
的参数包为空,但是依然能成功地编译运行并且输出计算结果true
。
可变参类模板
我们已经见识了很多函数模板中包展开的例子,但是这些并不是包展开的全部,接下来让我们了解一下在类的继承中形参包以及包展开是怎么使用的:
template<typename ...Args>
class Derived : public Args...
{
public:
Derived(const Args& ...args)
:Args(args)...
{}
};
class Base1
{
public:
Base1() {}
Base1(const Base1&)
{
std::cout << "copy ctor base1" << std::endl;
}
};
class Base2
{
public:
Base2() {}
Base2(const Base2&)
{
std::cout << "copy ctor Base2" << std::endl;
}
void base2_show()
{
}
};
int main()
{
Base1 b1;
Base2 b2;
Derived<Base1, Base2> d(b1, b2);
}
在上面的代码中,derived
是可变参数的类模板,有趣的地方是它将形参包作为自己的基类并且在其构造函数的初始化列表中对函数形参包进行了解包,其中Args(args)…
是包展开,Args(args)
是模式。到此为止大家应该对形参包和包展开有了一定的理解,现在是时候介绍另一种可变参数模板了,这种可变参数模板拥有一个模板形参包,请注意这里并没有输入或者打印错误,确实是模板形参包。之所以在前面没有提到这类可变参数模板,主要是因为它看起来过于复杂。
template<template<typename ...> typename ...Args>
class Bar : public Args<int, double>...
{
public:
Bar(const Args<int, double>&...args)
:Args<int, double>(args)...
{}
};
template<typename ...Args>
class Baz1 {};
template<typename ...Args>
class Baz2 {};
int main()
{
Baz1<int, double> a1;
Baz2<int, double> a2;
Bar<Baz1, Baz2>(a1, a2);
}
可以看到类模板bar
的模板形参是一个模板形参包,也就是说其形参包是可以接受零个或者多个模板的模板形参。在这个例子中,bar<baz1, baz2>
接受了两个类模板baz1
和baz2
。不过模板缺少
模板实参是无法实例化的,所以bar
实际上继承的不是baz1
和baz2
两个模板,而是它们的实例baz1<int, double>
和baz2<int,double>
。还有一个有趣的地方,template<template<class…> class…Args>
似乎存在两个形参包,但事实并非如此。因为最里面的template<typename…>
只说明模板形参是一个变参模板,它不能在bar
中被展开。
非类型模板参数
模板参数并不局限于定义类型,可以使用编译器内置类型作为参数,在编译期间变成模板的特定常量。我们甚至可以对这些参数使用默认值。
**示例:**编写一个封装了静态数组的类,类名为Array
template<typename T,size_t _size=10>
class Array
{
public:
T& operator[](int index)
{
return _arr[index];
}
size_t size()const
{
return _size;
}
private:
T _arr[_size]{0};
};
int main()
{
Array<int,5> arr;
arr[0] = 2;
arr[1] = 3;
for (size_t i = 0; i < arr.size(); i++)
{
std::cout << arr[i] << " ";
}
return 0;
}
可以看到模板定义了两个模板参数,第一个是类型为T
,第二个是用size_t
定义的_size
,并给了一个默认值。
别名模板
我们在写程序的过程中,总是希望写简短的代码,给一些复杂的名称简化,或者取一个简短的名字,于是又有了类型别名,用来重新定义复杂的名字。不管是模板还是非模板都需要类型别名,可以使用typedef
为模板具体化指定别名.
//模板别名
//1,typedef
typedef Array<int, 20> IntArr;
//2,using
using _IntArr = Array<int, 20>;
如果你经常编写类似于上面typedef
的代码,如果代码量过大的话,你可能会忘记能是什么意思,这样的话就不能叫做简化代码了,直接就是给自己找麻烦。而且typdef
定义不了别名模板,所以C++11
新增了一项功能——使用模板提供一系列别名(模板别名),例如:
//别名模板
//1,typedef
//template<typename T>
//typedef Array<T, 10> BigArray; //error C2823: typedef 模板 非法
//2,using
template<typename T>
using BigArray = Array<T, 128>;
这样定义好之后,使用BigArray就相当于使用Array<T,128>
。
变量模板(C++14)
变量模板定义一组变量或静态数据成员
定义格式
template<typename T>
T name = value;
name
:变量名value
:初始值
解释
从变量模板实例化的变量被称为被实例化变量,从静态数据成员模板实例化的变量被称为被实例化静态数据成员。
template<typename T>
constexpr T PI = T(3.14159265358L);
template<typename T>
T circle_area(T r)
{
return PI<T> *r * r; //PI<是变量模板实例化>
}
首先声明了一个变量模板PI
,在后面就可以使用各种类型的PI
了(如:PI<float>、PI<double>、PI<int>
等等),非常方便。值得注意的是,在声明变量模板T
的前面,加上了constexpr
关键字,这个关键字和const
类似,最大的区别就是:**const**
** 定义的常量也可能不是常量,但是用****constexpr**
定义的一定是常量。「const
定义的常量在运行时是不变的,但不一定在编译时就能确定其值;而 constexpr
定义的常量不仅在运行时不变,其值也必须在编译时就能确定**」**
int sum = 10;
const int csum = sum;
此处的csum
就不是一个常量,因为他必须在程序运行期间才能确定值,因为sum
在运行期间才有值,而他有依赖sum
。
constexpr int s = sum;
而使用constexpr
声明,则会报错error C2131: 表达式的计算结果不是常数
,所以constexpr
,只能声明真常量。这只是用在定义变量中,如果把constexpr
写在函数前面,则表示这个函数是返回的一个常量,编译器可以大胆的优化。
uint8_t CharMax()
{
return 127;
}
定义一个获取char
类型最大值的函数。
int max1 = CharMax();
定义一个int
类型对象,用于接受CharMax
的返回值,完全没问题。
const int max2 = CharMax();
定义一个const int
类型对象,用于接受CharMax
的返回值,完全没问题;只是max2
后续不能被修改。
constexpr int max3 = CharMax();
定义一个constexpr int
类型的对象,会发现直接报错error C2131: 表达式的计算结果不是常数
,就算你给CharMax
的返回类型加上const
也没用。这个时候就必须把函数声明为constexpr
了,这样编译器就会大胆优化,只要是出现CharMax()
的地方,直接用127
替代,是不是和宏很类似?确实,但是宏没有运行时安全检查。
注意:constexpr声明的函数,函数体不要写的太复杂
constexpr uint8_t CharMax()
{
int max = 117;
for (int i = 0; i < 10; i++)
{
++max;
}
return max;
}
这样写也是可以滴!这个代码是编译时编译器计算出来的,而不是运行时。
在类作用域中使用时,变量模板声明一个静态数据成员模板。与其他静态成员一样,静态数据成员模板的需要一个定义。这种定义可以在类定义外提供:
- 静态数据 成员模板
struct Limits
{
template<typename T>
static const T max;
};
template<typename T>
const T Limits::max = {};
- 类模板的非静态数据成员
template<typename T>
struct Foo
{
static const T foo;
};
template<typename T>
const T Foo<T>::foo = {};
其实如果静态变量声明的是const
,并且有初始值,那么可以不用在类外定义,会自动内联。
class Test
{
public:
static const int count = 0;
};
cout << Test::count << endl; //可以直接使用
注解
在C++14
引入变量模板前,参数化变量通常实现为类模板的静态数据成员,或返回所需值的 constexpr
函数模板。
//1,使用变量模板
template<typename T>
constexpr T PI = T(3.14159265358);
//2,使用函数模板
template<typename T>
constexpr T getPI()
{
return T(3.14159265358);
}
//3,使用类模板
template<typename T>
struct Math
{
static constexpr T PI = T(3.14159265358);
};
int main()
{
cout << PI<int> << " "<<PI<float> << endl;
cout << getPI<int>() << " " << getPI<float>() << endl;
cout << Math<int>::PI << " " << Math<float>::PI << endl;
return 0;
}
模板元编程库
C++ 提供元编程设施,诸如类型特性、编译时有理数算术,以及编译时整数序列。
类型属性
在标头 <type_traits>
定义。
is_void | 检查类型是否为 void (类模板) |
---|---|
is_null_pointer(C++14) | 检查类型是否为 std::nullptr_t (类模板) |
is_integral | 检查类型是否为整数类型 (类模板) |
is_floating_point | 检查类型是否是浮点类型 (类模板) |
is_array | 检查类型是否是数组类型 (类模板) |
is_enum | 检查类型是否是枚举类型 (类模板) |
is_union | 检查类型是否为联合体类型 (类模板) |
is_class | 检查类型是否非联合类类型 (类模板) |
is_function | 检查是否为函数类型 (类模板) |
is_pointer | 检查类型是否为指针类型 (类模板) |
is_lvalue_reference | 检查类型是否为_左值引用_ (类模板) |
is_rvalue_reference | 检查类型是否为_右值引用_ (类模板) |
is_member_object_pointer | 检查类型是否为指向非静态成员对象的指针 (类模板) |
is_member_function_pointer | 检查类型是否为指向非静态成员函数的指针 (类模板) |
is_fundamental | 检查是否是基础类型 (类模板) |
---|---|
is_arithmetic | 检查类型是否为算术类型 (类模板) |
is_scalar | 检查类型是否为标量类型 (类模板) |
is_object | 检查是否是对象类型 (类模板) |
is_compound | 检查是否为复合类型 (类模板) |
is_reference | 检查类型是否为_左值引用_或_右值引用_ (类模板) |
is_member_pointer | 检查类型是否为指向非静态成员函数或对象的指针类型 (类模板) |
is_const | 检查类型是否为 const 限定 (类模板) |
---|---|
is_volatile | 检查类型是否为 volatile 限定 (类模板) |
is_trivial | 检查类型是否平凡 (类模板) |
is_trivially_copyable | 检查类型是否可平凡复制 (类模板) |
is_standard_layout | 检查是否是一个标准布局类型 (类模板) |
has_unique_object_representations(C++17) | 检查是否该类型对象的每一位都对其值有贡献 (类模板) |
has_strong_structural_equality(C++20) | 检查类型是否拥有强结构相等性 (类模板) |
is_empty | 检查类型是否为类(但非联合体)类型且无非静态数据成员 (类模板) |
is_polymorphic | 检查类型是否为多态类类型 (类模板) |
is_abstract | 检查类型是否为抽象类类型 (类模板) |
is_final(C++14) | 检查类型是否为 final 类类型 (类模板) |
is_aggregate(C++17) | 检查类型是否聚合类型 (类模板) |
is_signed | 检查类型是否为有符号算术类型 (类模板) |
is_unsigned | 检查类型是否为无符号算术类型 (类模板) |
is_bounded_array(C++20) | 检查类型是否为有已知边界的数组类型 (类模板) |
is_unbounded_array(C++20) | 检查类型是否为有未知边界的数组类型 (类模板) |
is_scoped_enum(C++23) | 检查类型是否为有作用域枚举类型 (类模板) |
is_constructible is_trivially_constructible is_nothrow_constructible | 检查类型是否带有针对特定实参的构造函数 (类模板) |
---|---|
is_default_constructible is_trivially_default_constructible is_nothrow_default_constructible | 检查类型是否有默认构造函数 (类模板) |
is_copy_constructible is_trivially_copy_constructible is_nothrow_copy_constructible | 检查类型是否拥有复制构造函数 (类模板) |
is_move_constructible is_trivially_move_constructible is_nothrow_move_constructible | 检查类型是否能从右值引用构造 (类模板) |
is_assignable is_trivially_assignable is_nothrow_assignable | 检查类型是否拥有针对特定实参的赋值运算符 (类模板) |
is_copy_assignable is_trivially_copy_assignable is_nothrow_copy_assignable | 检查类型是否拥有复制赋值运算符 (类模板) |
is_move_assignable is_trivially_move_assignable is_nothrow_move_assignable | 检查类型是否有拥有移动赋值运算符 (类模板) |
is_destructible is_trivially_destructible is_nothrow_destructible | 检查类型是否拥有未被弃置的析构函数 (类模板) |
has_virtual_destructor | 检查类型是否拥有虚析构函数 (类模板) |
is_swappable_with is_swappable is_nothrow_swappable_with is_nothrow_swappable(C++17) | 检查一个类型的对象是否能与同类型或不同类型的对象交换 (类模板) |
alignment_of | 获取类型的对齐要求 (类模板) |
---|---|
rank | 获取数组类型的维数 (类模板) |
extent | 获取数组类型在指定维度的大小 (类模板) |
is_same | 检查两个类型是否相同 (类模板) |
---|---|
is_base_of | 检查一个类型是否派生自另一个类型 (类模板) |
is_convertible is_nothrow_convertible(C++20) | 检查是否能转换一个类型为另一类型 (类模板) |
is_invocable is_invocable_r is_nothrow_invocable is_nothrow_invocable_r(C++17) | 检查类型能否以给定的实参类型调用(如同以 std::invoke) (类模板) |
reference_constructs_from_temporary(C++23) | 检查在直接初始化中引用是否绑定到临时对象 (类模板) |
reference_converts_from_temporary(C++23) | 检查在复制初始化中引用是否绑定到临时对象 (类模板) |
is_layout_compatible(C++20) | 检查二个类型是否_布局兼容_ (类模板) |
is_pointer_interconvertible_base_of(C++20) | 检查一个类型是否为另一类型的_指针可互转换_(起始)基类 (类模板) |
is_pointer_interconvertible_with_class(C++20) | 检查一个类型的对象是否与该类型的指定子对象指针可互转换 (函数模板) |
is_corresponding_member(C++20) | 检查二个指定成员是否在二个指定类型中的公共起始序列中彼此对应 (函数模板) |
在标头 <type_traits> 定义 | |
---|---|
conjunction(C++17) | 变参的逻辑与元函数 (类模板) |
disjunction(C++17) | 变参的逻辑或元函数 (类模板) |
negation(C++17) | 逻辑非元函数 (类模板) |
模板类 | 描述 |
---|---|
integral_constantbool_constant(C++17) | 具有指定值的指定类型的编译期常量 (类模板) |
标准提供 std::integral_constant 对类型 bool 的二个特化:
在标头 <type_traits> 定义 | |
---|---|
类型 | 定义 |
true_type | std::integral_constant<bool, true> |
false_type | std::integral_constant<bool, false> |
类型修改
这些类型特性应用到模板参数的修改,并(有时条件性地)声明成员 typedef type
为结果类型。试图特化定义于 <type_traits>
头文件且描述于本节的模板导致未定义行为,除了可以按描述要求特化 std::common_type 与 std::basic_common_reference
(C++20 起) 。除非另外有指定,可以用不完整类型实例化定义于 <type_traits>
头文件的模板,尽管通常禁止以不完整类型实例化标准库模板。
remove_cv remove_const remove_volatile | 从给定类型移除 const 和/或 volatile 限定符 (类模板) |
---|---|
add_cv add_const add_volatile | 添加 const 和/或 volatile 限定符到给定类型 (类模板) |
remove_reference | 从给定类型移除引用 (类模板) |
---|---|
add_lvalue_reference add_rvalue_reference | 向给定类型添加_左值_或_右值_引用 (类模板) |
remove_pointer | 移除给定类型的一层指针 (类模板) |
---|---|
add_pointer | 对给定类型添加一层指针 (类模板) |
make_signed | 使给定的整数类型有符号 (类模板) |
---|---|
make_unsigned | 使给定的整数类型无符号 (类模板) |
remove_extent | 从给定数组类型移除一个维度 (类模板) |
---|---|
remove_all_extents | 移除给定数组类型的所有维度 (类模板) |
杂项变换
decay | 实施当按值传递实参给函数时所进行的类型变换 (类模板) |
---|---|
remove_cvref(C++20) | 将 std::remove_cv 与 std::remove_reference 结合 (类模板) |
enable_if | 条件性地从重载决议移除函数重载或模板特化 (类模板) |
conditional | 基于编译时布尔值选择一个类型或另一个 (类模板) |
common_type | 确定一组类型的公共类型 (类模板) |
common_reference basic_common_reference(C++20) | 确定类型组的共用引用类型 (类模板) |
underlying_type | 获取给定枚举类型的底层整数类型 (类模板) |
void_t(C++17) | void 变参别名模板 (别名模板) |
type_identity(C++20) | 返回不更改的类型实参 (类模板) |
static_assert
static_assert
是从C++
关键字,static_assert
可让编译器在编译时进行断言检查。static_assert
的语法格式如下:
static_assert(constant-expression, string-literal ); // C++11
static_assert(constant-expression); // C++17
constant-expression
:可转换为布尔值的整型常量表达式。 如果计算出的表达式为零 (false
),则显示string-literal
参数,并且编译失败,并出现错误。 如果表达式不为零 (true
),则static_assert
声明无效。string-literal
:当constant-expression
参数为零时显示的消息。C++ 17
前需要string-literal
消息参数,C++17
变成可选。
static_assert
可以用在全局作用域中,命名空间中,类作用域中,函数作用域中,几乎可以不受限制的使用。
比如下面的断言,当使用32位编译程序时,断言为false
,中断编译并输出提示消息 error C2338: static_assert failed: '32-bit code generation is not supported.'
static_assert(sizeof(void *) == 8, "32-bit code generation is not supported.");
下面对函数传入的参数类型进行断言,如果传递的是const char*
则断言失败!
template<typename T>
T add(T a, T b)
{
static_assert(std::is_same_v<T, const char*>, "<const char*>类型不能相加");
return a + b;
}
下面对非类型模板参数进行断言,如果传递的SIZE==0
则断言失败!
template<typename T, size_t SIZE>
class Array
{
static_assert(SIZE != 0, "The array size cannot be zero");
};
SFINAE
SFINAE
可以说是C++
模板进阶的门槛之一,我们不用纠结这个词的发音,它来自于 Substitution failure is not an error
的首字母缩写,意为替换失败并非错误。
有以下函数模板:它可以支持任意类型的相加。
template<typename T>
auto add(T a, T b)
{
return a + b;
}
std::cout << add(1, 2) << std::endl;
std::cout << add('A', '1') << std::endl;
std::cout << add(1.2, 2.6) << std::endl;
但是如果传入的类型并不支持相加,就会报如下错误:
std::cout << add("hello", " world") << std::endl;

这个错误并不能很好的让我们知道为什么错,很疑惑,所以我们必须想办法,让他的错误原因更加细致。
- 使用
decltype
推导结果类型
template<typename T>
auto add(T a, T b) -> decltype(a + b);
此时错误信息如下所示:

- 使用
static_assert
进行断言
template<typename T>
auto add(T a, T b)
{
static_assert(std::is_same_v<T, const char*>, "<const char*>类型不能相加");
return a+b;
}
此时错误信息如下:

- 当然还可以直接使用
if constexpr
template<typename T>
auto add(T a, T b)
{
if constexpr (std::is_same_v<T, const char*>)
{
static char buf[1024];
strcpy(buf, a);
strcat(buf, b);
return buf;
}
return a + b;
}
- 使用
std::enable_if_t
template<typename T>
auto add(T a, T b)
-> std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>, T>
{
return a + b;
}
//或(这才是最常用的用法)
template<typename T, std::enable_if_t<std::is_integral_v<T> || std::is_floating_point_v<T>, int> = 0>
auto add(T a, T b)
{
return a + b;
}
错误信息如下:
