可变参数模板
基本语法及原理
-
C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被作为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数
-
格式:
- 注意模板是...Args,参数类型是Args...
- template<class ...Args> void Func(Args... args) {}
- template<class ...Args> void Func(Args&... args) {}
- template<class ...Args> void Func(Args&&... args) {}
-
用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,class...或typedef...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示。每个参数实例化时遵循折叠规则。
-
用 sizeof... 运算符计算参数包中参数的个数(不是sizeof,这是一个新的运算符)
template <class ...Args>
void Print(Args&&... args) //0-N个参数
{
cout << sizeof...(args) << endl;
}int main()
{
double x = 2.2;
Print(); //包里有0个参数
Print(1); //包里有1个参数
Print(1, string("xxx")); //包里有2个参数
Print(1, string("xxx"), x); //包里有3个参数return 0;}
//原理1:编译本质这里会结合引用折叠规则实例化出以下4个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3); //x是左值,所以double&//原理2:更本质去看有没有可变参数模板,我们是先出这样的多个函数模板才能支持
// 这里的功能,有了可变参数模板,我们进一步被解放,他是类型泛化基础上
// 叠加数量变化,让我们泛型编程更灵活
void Print();template <class T1>
void Print(T1&& arg1);template <class T1, class T2>
void Print(T1&& arg1, T2&& arg2);template <class T1, class T2, class T3>
void Print(T1&& arg1, T2&& arg2, T3&& arg3);
包扩展
-
包扩展就是把它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。
-
我们通过在模式的右边放一个省略号(...)来触发扩展操作
-
可变参数模板是编译时解析,而for(size_t i = 0; i < sizeof...(args); i++)是运行时获取和编译,所以不支持这样使用
-
参数包展开的格式是
表达式(参数包)...,...必须写在整个表达式的末尾,用来触发=="对每个参数都执行一次表达式" ==的展开逻辑。例如:GetArg(args)...的作用是:把参数包里的每个元素,分别传给GetArg函数,然后把所有GetArg的返回值,组成一个新的参数列表void ShowList()
{
//编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
cout << endl;
}template<class T,class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
//args是N个参数的参数包,调用ShowList,参数包的第一个传给x,剩下N-1个传给第二个参数包
ShowList(args...);
}template<class ...Args>
void Print(Args... args)
{
ShowList(args...);
}int main()
{
Print();
Print(1);
Print(1, string("xxx"));
Print(1, string("xxx"), 2.2);return 0;}
//Print(1, string("xxxxx"), 2.2);调用时
//本质编译器将可变参数模板通过模式的包扩展,
//编译器推导的以下三个重载函数函数
void ShowList(double z)
{
cout << z << " ";
ShowList();
}
void ShowList(string y, double z)
{
cout << y << " ";
ShowList(z);
}void ShowList(int x, string y, double z)
{
cout << x << " ";
ShowList(y, z);
}void Print(int x, string y, double z)
{
ShowList(x, y, z);
}
![[包扩展底层细节.png]]
template <class T>
const T& GetArg(const T& x)
{
cout << x << " ";
return x;
}
template <class ...Args>
void Arguments(Args... args)
{
}
template <class ...Args>
void Print(Args... args)
{
Arguments(GetArg(args)...);
//GetArg(args)...表示要展开,传入三个参数
//实例化出Arguments(GetArg(x), GetArg(y), GetArg(z));
//编译时展开为:Arguments(GetArg(1), GetArg(string("xxxxx")), GetArg(2.2));
}
int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}
emplace接口
-
emplace系列的接口均为模板可变参数,功能上兼容push和insert系列。
-
emplace还支持直接插入构造T对象的参数,可直接在容器空间上构造T对象
-
纯粹的左值插入和右值插入emplace_back和push_back是一样的;部分场景下,emplace可以直接构造,push和insert是 构造+移动构造 或 构造+拷贝构造
-
综上,emplace更好用且强大,推荐用emplace系列替代push和insert
int main()
{
listssp::string lt;
//传左值,跟push_back一样,走拷贝构造
ssp::string s1("111111"); //string(char* str) -- 构造
lt.emplace_back(s1); //string(const string& s) -- 拷贝构造
cout << "*********************************" << endl;//右值,跟push_back一样,走移动构造 lt.emplace_back(move(s1)); //string(string&& s) -- 移动构造 cout << "*********************************" << endl; //直接把构造string参数包往下传,直接用string参数包构造string //这里达到的效果是push_back做不到的 lt.emplace_back("11111"); //string(char* str) -- 构造 cout << "*********************************" << endl; list<pair<ssp::string, int>> lt1; //跟push_back一样 //构造pair + 拷贝/移动构造pair到list的节点中data上 pair<ssp::string, int> kv("苹果", 1); //string(char* str) -- 构造 lt1.emplace_back(kv); //string(const string& s) -- 拷贝构造 cout << "*********************************" << endl; //跟push_back一样 lt1.emplace_back(move(kv)); //string(string&& s) -- 移动构造 cout << "*********************************" << endl; //直接把构造pair参数包往下传,直接⽤pair参数包构造pair //这里达到的效果是push_back做不到的 lt1.emplace_back("苹果", 1); //string(char* str) -- 构造 cout << "*********************************" << endl; return 0;}
-
万能引用和完美转发使用
//构造函数
template <class ...Args>
ListNode(Args&&... args)
: _next(nullptr)
, _prev(nullptr)
, _data(std::forward(args)...)
{}//emplace_back实现
template void emplace_back(Args&&... args)
{
insert(end(), std::forward(args)...);
}
...的所有位置
声明参数包
模板参数列表中:声明类型参数包
-
格式:
template <class... Args>或template <typename... Args>// ... 跟在Args后面,声明Args是"类型参数包"(装0~N个类型)
template <class... Args>
void Print(Args&&... args); // 这里的Args就是上面声明的类型包
函数参数列表中:声明值参数包
-
格式:格式:
void Func(T... args)(T 是类型,args 是值参数包)// 先声明类型包Ts,再用Ts...声明值参数包args
template <typename... Ts>
void Func(Ts... args)
{
// args是值参数包,装0~N个值
}
展开参数包
函数调用中展开
-
格式:函数名(表达式(参数包)...)
-
关键:
GetArg(args)是 "包含参数包的表达式",...跟在这个表达式后面,才会对每个参数执行一次GetArgtemplate <class... Args>
void Print(Args&&... args)
{
// 核心:GetArg(args)... 是"表达式(参数包)...",...在末尾触发展开
Arguments(GetArg(args)...);
// 展开逻辑(编译时):
// Print(1, "xxx", 2.2) → Arguments(GetArg(1), GetArg("xxx"), GetArg(2.2))
}
初始化列表 / 数组中展开
-
格式:{表达式(参数包)...}
template <typename... Ts>
void PrintSize(Ts... args)
{
// 展开参数包,计算每个参数的大小,存入数组
int sizes[] = {sizeof(args)...};
// 展开逻辑:
// PrintSize(1, "xxx", 2.2) → int sizes[] = {sizeof(1), sizeof("xxx"), sizeof(2.2)};
for (auto s : sizes)
{
cout << s << " ";
// 输出:4 4 8(不同编译器可能有差异)
}
}
递归展开
-
格式:函数名(表达式(参数包)...)(递归调用自身,逐步拆包)
// 递归终止函数(参数包为空时调用)
void RecursivePrint() {
cout << endl;
}// 递归展开函数(拆出第一个参数,剩下的继续递归)
template <typename T, typename... Ts>
void RecursivePrint(T first, Ts... rest) {
cout << first << " ";
RecursivePrint(rest...); // ...跟在rest后面,展开剩余参数包
}// 调用:RecursivePrint(1, "xxx", 2.2);
// 执行流程:
// 1. RecursivePrint(1, "xxx", 2.2) → 输出1,调用RecursivePrint("xxx", 2.2)
// 2. RecursivePrint("xxx", 2.2) → 输出xxx,调用RecursivePrint(2.2)
// 3. RecursivePrint(2.2) → 输出2.2,调用RecursivePrint()
// 4. RecursivePrint() → 输出换行,结束
万能引用中&& + ...
template <class... Args>
void Print(Args&&... args) { // Args&&... 是"万能引用的参数包"
// Args&& 是万能引用,... 是参数包展开,两者结合表示"0~N个万能引用参数"
}
新的类功能
默认的移动构造和移动赋值
-
在之前的C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const取地址重载
-
C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载
-
如果没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动构造函数。
-
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝;自定义类型成员,则需要看这个成员是否实现了移动构造,如果实现了就是调用移动构造,没有实现就调用拷贝构造
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
,_age(age)
{ }
private:
ssp::string _name;
int _age;
};int main()
{
Person s1; //string(char* str) -- 构造
Person s2 = s1; //string(const string& s) -- 拷贝构造
Person s3 = std::move(s1); //string(string&& s) -- 移动构造
Person s4; //string(char* str) -- 构造
s4 = std::move(s2); //string& operator=(string&& s) -- 移动赋值return 0;}
成员变量声明时给的缺省值
- 成员变量声明时给的缺省值是给初始化列表用的,如果没有显示在初始化列表初始化,就会在初始化列表用这个值初始化
- 使用优先级:显示传参 > 有初始化列表的参数列表中的缺省值 > 无初始化列表的声明时给的缺省值
- 没有初始化列表就用声明时给的缺省值,有的话就用显示传参/参数列表的缺省值
default和delete
-
default强制生成某个默认的函数,比如我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成
-
想要限制某些默认函数的生成,只需在该函数声明加上 =delete 即可,该语法指示编译器不生产对应函数的默认版本,称 =delete修饰的函数为删除函数
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
,_age(age)
{ }//Person(const Person& p) // :_name(p._name) // ,_age(p._age) //{ } Person(Person&& p) = default; //强制生成移动构造 Person(const Person& p) = delete; //限制生成拷贝构造(禁止调用)private:
ssp::string _name;
int _age;
};int main()
{
Person s1;
//Person s2 = s1; //不能使用拷贝构造
Person s3 = std::move(s1);
return 0;
}
final和override
-
不想让派生类重写某个虚函数,可以用final修饰
class Car {
public:
virtual void Dirve() final
{}
};class Benz :public Car
{
public:
// error C3248: "Car::Dirve": 声明为 "final" 的函数不能由 "Benz::Dirve" 重写
virtual void Dirve()
{
cout << "Benz舒适" << endl; }
};
} -
override可以帮助用户检测是否正确重写了虚函数(若是重写正确无影响,不正确会直接报错)
class Car {
public:
virtual void Dirve()
{}
};class Benz :public Car
{
public:
virtual void Dirve() override
{
cout << "Benz舒适" << endl;
}
};
STL中有哪些新变化
- STL中新添了一些新容器,unordered_map,unordered_set,array,forward_list
- STL容器中增加了新接口,最重要的就是右值引用和移动语义相关的push/insert/emplace系列接口,移动构造和移动赋值,还有initializer_list版本的构造
- 容器的范围for遍历