C++11(可变参数模板、新的类功能和STL中的一些变化)
- [1. 可变参数模板](#1. 可变参数模板)
-
- [1.1 基本语法及原理](#1.1 基本语法及原理)
- [1.2 包扩展](#1.2 包扩展)
- [1.3 emplace系列接口](#1.3 emplace系列接口)
- [2. 新的类功能](#2. 新的类功能)
-
- [2.1 默认的移动构造和移动赋值](#2.1 默认的移动构造和移动赋值)
- [2.2 成员变量声明时给缺省值](#2.2 成员变量声明时给缺省值)
- [2.3 default和delete](#2.3 default和delete)
- [2.4 final和override](#2.4 final和override)
- [3. STL中的一些变化](#3. STL中的一些变化)
1. 可变参数模板
1.1 基本语法及原理
- C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
- template <class ...Args> void Func(Args... args) {}
- template <class ...Args> void Func(Args&... args) {}
- template <class ...Args> void Func(Args&&... args) {}
- 我们⽤省略号来指出⼀个模板参数或函数参数表示的⼀个包,在模板参数列表中,class...或typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后⾯跟...指出接下来表示零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表示,跟前⾯普通模板⼀样,每个参数实例化时遵循引⽤折叠规则。
- 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
- 这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数。
cpp
template <class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl; // sizeof...运算符编译时计算参数包中参数的个数
}
int main()
{
double x = 2.2;
Print(); // 包里有0个参数
Print(1); // 包里有1个参数
Print(1, string("xxxxx")); // 包里有2个参数
Print(1.1, string("xxxxx"), x); // 包里有3个参数
return 0;
}
// 原理1:编译器本质这⾥会结合引⽤折叠规则实例化出以下四个函数
void Print();
void Print(int&& arg1);
void Print(int&& arg1, string&& arg2);
void Print(double&& arg1, string&& arg2, double& arg3);
// 原理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);
// ...

1.2 包扩展
- 对于⼀个参数包,我们除了能计算它的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层的实现细节如下图所示。
- C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。

cpp
// 可变模板参数
// 参数类型可变
// 参数个数可变
// 打印参数包内容
template <class ...Args>
void Print(Args... args)
{
// 可变参数模板编译时解析
// 下⾯是运⾏获取和解析,所以不⽀持这样⽤
cout << sizeof...(args) << endl;
//err
for (size_t i = 0; i < sizeof...(args); i++)
{
cout << args[i] << " ";
}
cout << endl;
}
要递归推演的包扩展
cpp
void ShowList()
{
// 编译时递归的终止条件
cout << endl;
}
//err
//template <class T, class ...Args>
//void ShowList(T x, Args... args)
//{
// cout << x << " ";
//
// // 运行时条件判定,所以不能这样写
// if (sizeof...(args) == 0)
// return;
//
// ShowList(args...); // 编译时递归
//}
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("xxxxx"));
Print(1, string("xxxxx"), 2.2);
return 0;
}

cpp
void ShowList()
{
cout << endl;
}
void ShowList(double x)
{
cout << x << " ";
ShowList();
}
void ShowList(string x, double x3)
{
cout << x << " ";
ShowList(x3);
}
void ShowList(int x, string x2, double x3)
{
cout << x << " ";
ShowList(x2, x3);
}
void Print(int x1, string x2, double x3)
{
ShowList(x1, x2, x3);
}
int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}

不用递归推演的包扩展
cpp
template <class T>
const T& GetArg(const T& x)
{
cout << x << " ";
return x;
}
//template <class T>
//int GetArg(const T& x)
//{
// cout << x << " ";
// // GetArg返回什么不重要,只要返回N个参数就行
// // 因为返回的参数在Arguments中什么也不干
// return 0;
//}
template <class ...Args>
void Arguments(Args... args)
{}
template <class ...Args>
void Print(Args... args)
{
// 注意GetArg必须返回获得到的对象,这样才能组成参数包给Arguments
// GetArg的返回值组成实参参数包,传给Arguments
Arguments(GetArg(args)...); // 这个不用递归推演,就是包扩展
}
// 本质可以理解为编译器编译时,包的扩展模式将上⾯的函数模板扩展实例化为下⾯的函数
//void Print(int x, string y, double z)
//{
// Arguments(GetArg(x), GetArg(y), GetArg(z));
//}
int main()
{
Print(1, string("xxxxx"), 2.2);
return 0;
}

为什么是倒序打印?
函数调用参数求值顺序:
在C++标准中,函数参数的求值顺序是未指定的
大多数编译器的实际实现是从右到左 求值
所以实际执行可能是:GetArg(2.2)→ GetArg(string("xxxxx"))→ GetArg(1)
1.3 emplace系列接口
- template <class... Args> void emplace_back (Args&&... args);
- template <class... Args> iterator emplace (const_iterator position, Args&&... args);
- C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container< T >,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
- emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
- 第⼆个程序中我们模拟实现了list的emplace和emplace_back接⼝,这⾥把参数包不段往下传递,最终在结点的构造中直接去匹配容器存储的数据类型T的构造,所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
- 传递参数包过程中,如果是Args&&... args 的参数包,要⽤完美转发参数包,⽅式如下std::forward< Args >(args)... ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。
cpp
// emplace系列总体而言更高效,推荐以后使用emplace系列替代insert和push系列
int main()
{
std::list<bs::string> lt1;
// 传左值,跟push_back一样,走拷贝构造
bs::string s1("1111111111111");
lt1.push_back(s1);
lt1.emplace_back(s1);
cout << "**********************************" << endl;
// 传右值,跟push_back一样,走移动构造
bs::string s2("11111111111111111");
lt1.push_back(move(s2));
bs::string s3("111111111111111111");
lt1.emplace_back(move(s3));
cout << "**********************************" << endl;
// emplace_back的效率略高一筹
lt1.push_back("1111111111111111111"); // 单参数构造的隐式类型转换
// 直接把构造string参数包往下传,直接用string参数包构造string
lt1.emplace_back("1111111111111111111");
cout << "**********************************" << endl;
std::list<pair<bs::string, int>> lt2;
// 传左值,跟push_back一样,走拷贝构造
pair<bs::string, int> kv1("11111111111", 1);
lt2.push_back(kv1);
lt2.emplace_back(kv1);
cout << "**********************************" << endl;
// 传右值,跟push_back一样,走移动构造
pair<bs::string, int> kv2("11111111111", 1);
lt2.push_back(move(kv2));
pair<bs::string, int> kv3("11111111111", 1);
lt2.emplace_back(move(kv3));
cout << "**********************************" << endl;
// emplace_back的效率略高一筹
lt2.push_back({ "1111111111111111111", 1 }); // 多参数构造的隐式类型转换
// 花括号{},编译器会识别为initializer list,而initializer list的参数类型要相同
// 所以编译器报错: "initializer list": 不是"_Valty"的有效模板参数
// lt2.emplace_back({ "1111111111111111111", 1 }); // 不支持
// 参数包传下去,最后直接构造容器上的对象
lt2.emplace_back("1111111111111111111", 1);
cout << "**********************************" << endl;
return 0;
}

cpp
//list.h
#pragma once
//list.h
// 无关接口删除了
namespace bs
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _next = nullptr;
list_node<T>* _prev = nullptr;
//list_node(const T& val)
// :_data(val)
// , _next(nullptr)
// , _prev(nullptr)
//{}
//// insert中的val传到这
//list_node(T&& val = T())
// // 再把val强转成右值属性
// // val会去调用bs:string的移动构造
// :_data(move(val))
// , _next(nullptr)
// , _prev(nullptr)
//{}
template<class... Args>
list_node(Args&&... args)
: _data(forward<Args>(args)...)
, _next(nullptr)
, _prev(nullptr)
{}
list_node() = default;
// 这里写万能引用效果不好
// 得加一个默认构造,因为我们在new Node时,
// 函数模板的X是泛型,还未实例化
template<class X>
list_node(X&& val)
:_data(forward<X>(val))
, _next(nullptr)
, _prev(nullptr)
{}
};
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T, Ref, Ptr> Self;
Node* _node;
__list_iterator(Node* node)
:_node(node)
{
}
Ref operator*()
{
return _node->_data;
}
Self& operator++()
{
_node = _node->_next;
return *this;
}
bool operator!=(const Self& it) const
{
return _node != it._node;
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
//// 左值引用
//void push_back(const T& x)
//{
// insert(end(), x);
//}
//// 右值引用
//void push_back(T&& x)
//{
// insert(end(), move(x));
//}
// 万能引用
template<class X>
void push_back(X&& x)
{
insert(end(), forward<X>(x));
}
template<class... Args>
void emplace_back(Args&&... args)
{
emplace(end(), forward<Args>(args)...);
}
template<class... Args>
iterator emplace(iterator pos, Args&&... args)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(forward<Args>(args)...);
//prev newnode cur
prev->_next = newnode;
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
++_size;
//返回新插入节点位置的迭代器
return iterator(newnode);
}
//iterator insert(iterator pos, const T& val)
//{
// Node* cur = pos._node;
// Node* prev = cur->_prev;
// Node* newnode = new Node(val);
// //prev newnode cur
// prev->_next = newnode;
// newnode->_next = cur;
// cur->_prev = newnode;
// newnode->_prev = prev;
// ++_size;
// //返回新插入节点位置的迭代器
// return iterator(newnode);
//}
//// 假设val具有右值属性,那val应该具有常性,不能改变
//// 但是移动构造/移动赋值需要改变它,所以val必须具有左值属性
//iterator insert(iterator pos, T&& val)
//{
// Node* cur = pos._node;
// Node* prev = cur->_prev;
// // 既然val具有左值属性,那么要想调用移动构造
// // 要把val强转成右值属性传过去才行
// Node* newnode = new Node(move(val));
// //prev newnode cur
// prev->_next = newnode;
// newnode->_next = cur;
// cur->_prev = newnode;
// newnode->_prev = prev;
// ++_size;
// //返回新插入节点位置的迭代器
// return iterator(newnode);
//}
// X的类型是实参传递给形参推出来的
// 万能引用
template<class X>
iterator insert(iterator pos, X&& val)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(forward<X>(val));
//prev newnode cur
prev->_next = newnode;
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
++_size;
//返回新插入节点位置的迭代器
return iterator(newnode);
}
private:
Node* _head;
size_t _size = 0;
};
}
cpp
// 换成我们自己写的list,方便观察
#include "list.h"
int main()
{
bs::list<bs::string> lt1; // 我们自己写的有一个哨兵位
cout << "**********************************" << endl;
// 传左值,跟push_back一样,走拷贝构造
bs::string s1("1111111111111");
lt1.push_back(s1);
lt1.emplace_back(s1);
cout << "**********************************" << endl;
// 传右值,跟push_back一样,走移动构造
bs::string s2("11111111111111111");
lt1.push_back(move(s2));
bs::string s3("111111111111111111");
lt1.emplace_back(move(s3));
cout << "**********************************" << endl;
// emplace_back的效率略高一筹
lt1.push_back("1111111111111111111"); // 单参数构造的隐式类型转换
// 直接把构造string参数包往下传,直接用string参数包构造string
lt1.emplace_back("1111111111111111111");
cout << "**********************************" << endl;
bs::list<pair<bs::string, int>> lt2;// 我们自己写的有一个哨兵位
cout << "**********************************" << endl;
// 传左值,跟push_back一样,走拷贝构造
pair<bs::string, int> kv1("11111111111", 1);
lt2.push_back(kv1);
lt2.emplace_back(kv1);
cout << "**********************************" << endl;
// 传右值,跟push_back一样,走移动构造
pair<bs::string, int> kv2("11111111111", 1);
lt2.push_back(move(kv2));
pair<bs::string, int> kv3("11111111111", 1);
lt2.emplace_back(move(kv3));
cout << "**********************************" << endl;
// emplace_back的效率略高一筹
// 下面这句代码不能用万能引用,因为花括号初始化列表本身没有具体的类型信息
// 模板参数 x 无法被推导出来
// 模板参数推导规则:
// 1. 模板参数推导需要明确的类型信息
// 2. 但{ ... }在编译期没有确定的类型
// 3. 编译器不知道 x 应该是什么类型
//lt2.push_back({ "1111111111111111111", 1 }); // 多参数构造的隐式类型转换
// 把类型给出来就行了,或者不用万能引用,用一个左值和一个右值引用
// 编译器底层就是用一个左值和一个右值引用
lt2.push_back(std::pair<bs::string, int>("11111111111111111111", 1));
// 花括号{},编译器会识别为initializer list,而initializer list的参数类型要相同
// 所以编译器报错: "initializer list": 不是"_Valty"的有效模板参数
// lt2.emplace_back({ "1111111111111111111", 1 }); // 不支持
// 参数包传下去,最后直接构造容器上的对象
lt2.emplace_back("1111111111111111111", 1);
cout << "**********************************" << endl;
return 0;
}

2. 新的类功能
2.1 默认的移动构造和移动赋值
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
- 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀个。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
- 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数、拷⻉构造、拷⻉赋值重载中的任意⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动赋值函数,对于内置类型成员会执⾏逐成员按字节移动,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。
2.2 成员变量声明时给缺省值
成员变量声明时给缺省值是给初始化列表⽤的,如果没有显式在初始化列表初始化,就会在初始化列表⽤这个缺省值初始化
2.3 default和delete
C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显式指定移动构造⽣成。
如果想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明不定义,这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。
2.1 / 2.2 / 2.3
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
: _name(name)
, _age(age)
{}
// 委托构造
Person(int i, const char* name = "", int age = 0)
:Person(name, age)
{
_i = i;
}
// 强制不让生成,不期望这个类的对象被拷贝
//Person(const Person& p) = delete;
//Person(const Person& p)
//:_name(p._name)
//,_age(p._age)
//{}
// 强制生成
//Person(Person&& p) = default;
//Person& operator=(const Person& p)
//{
// if(this != &p)
// {
// _name = p._name;
// _age = p._age;
// }
// return *this;
//}
//~Person()
//{}
private:
bs::string _name;
int _age;
int _i = 0;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = move(s1);
Person s4;
s4 = move(s2);
}
2.4 final和override
我截图了C++多态对final和override的描述,具体可以去C++多态文章里了解。

3. STL中的一些变化
- 下图圈起来的就是STL中的新容器,但是实际最有⽤的是unordered_map和unordered_set。这两个我们前⾯已经进⾏了⾮常详细的讲解,其他的⼤家了解⼀下即可。
- STL中容器的新接⼝也不少,最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列接⼝和移动构造和移动赋值,还有initializer_list版本的构造等,这些前⾯都讲过了,还有⼀些⽆关痛痒的如cbegin/cend等需要时查查⽂档即可。
- 容器的范围for遍历,这个在容器部分也讲过了。
