目录
C++11相关新特性
列表初始化
初始化简单变量
初始化容器
decltype关键字
C++ 11新增的容器
左值引用和右值引用
左值与右值
左值引用与右值引用
左值引用和右值引用的相互转化
右值引用的使用
拷贝构造函数与移动构造函数
赋值重载函数与移动赋值重载函数
元素插入相关函数
万能引用与完美转发
万能引用
完美转发
C++ 11新增的两个默认成员函数
生成默认成员函数=default
不生成默认成员函数=delete
C++ 11中的可变参数模版
可变参数模版介绍
参数包展开
可变参数模版的应用emplace系列函数
emplace系列函数和push系列函数的选择
C++11相关新特性
列表初始化
初始化简单变量
在C语言和C++98中,对一中类型的变量进行初始化时,主要使用赋值符号与初始化值,如果是一个数组或者结构体等具有多个成员的变量初始化时,可以使用{}
进行初始化
struct point
{
int x;
int y;
};
int main()
{
// 初始化一个内置类型变量
int a = 0;
// 初始化一个数组
int data[] = { 2,3,5,4 };
// 初始化结构
point p = { 2, 1 };
return 0;
}
上面的代码中需要注意对于一个数组和结构来说,为了兼容C语言,默认是使用{}
进行初始化,此时不算作列表初始化
在C++11中,可以使用{}
对所有类型的变量进行初始化,并且可以省略赋值符号
struct point
{
int x;
int y;
};
int main()
{
// 列表初始化并省略赋值符号
int a1{ 0 };
int data1[]{ 1,2,3,4 };
point p1{ 2, 1 };
return 0;
}
需要注意,如果使用了列表初始化,则不可以出现部分用于初始化的值赋值给变量后出现数据丢失的情况,例如double
的值赋值给int
的变量
// double转int
double num1 = 3.14;
int a3 = { num1 };
// long long 转 int
long long num = 648797LL;
int a2 = {num};
// long 转 int
// 正常编译
long num2 = 648797L;
int a4 = { num2 };
报错信息:
conversion from '__int64' to 'int' requires a narrowing conversion
conversion from 'double' to 'int' requires a narrowing conversion
初始化容器
有了列表初始化后,容器的初始化可以变得更加简单,对比下面的初始化方式
int main()
{
// 初始化vector
// C++ 98的初始化
int data[] = { 0,1,2,3,4,5,6 };
vector<int> v;
for (auto num : data)
{
v.push_back(num);
}
// C++ 11的初始化
vector<int> v{ 0,1,2,3,4,5,6 };
return 0;
}
对于map来说,对比下面的初始化方式
int main()
{
// 初始化map
// C++ 98的初始化
map<int, int> m;
m.insert({ 1, 1 });
m.insert({ 2, 2 });
m.insert({ 3, 3 });
m.insert({ 4, 4 });
m.insert({ 5, 5 });
// C++ 11的初始化
map<int, int> m1{{ 1,1 }, { 2,2 }, { 3,3 }};
return 0;
}
在上面的代码中,对于C++ 98的初始化来说,通过多参数构造的隐式类型转换作为参数传递给insert()
函数,而C++ 11中,结合了类型转换已经列表初始化对map进行初始化
decltype
关键字
前面声明变量时使用auto
关键字,根据赋值符号右侧类型推导变量的类型,但是如果没有赋值,auto
此时不可以进行推导;为了知道某一个变量的类型,可以使用typeid(变量).name()
进行获取,直接打印即可查看指定变量的类型
但是上面两种方式都无法做到根据已有变量/常量的类型创建新的变量,为了解决这个问题,在C++ 11中新增了decltype
关键字
int main()
{
int x = 0;
// 获取普通变量类型创建变量
decltype(x) x1 = 2;
// 获取表达式的值类型创建变量
decltype(1 + 2) x2 = 3;
decltype(1 + 2.1) x3 = 3;
cout << typeid(x1).name() << endl;
cout << typeid(x2).name() << endl;
cout << typeid(x3).name() << endl;
return 0;
}
输出结果:
int
int
double
C++ 11新增的容器
下面红色标记的容器均为C++11新增的容器
<array>
:封装的是C语言静态数组,本质还是普通数组,只是为了便于控制数组越界等问题,因为一般的数组越界读写在编译阶段是不容易检测出来的
<forward_list>
:单链表,这个单链表没有尾插和尾删,因为开销大
<unordered_map>
和<unordered_set>
:封装的是哈希表
左值引用和右值引用
左值与右值
左值代表赋值符号左侧的值,可以直接取地址,一般为变量,并且一般情况下可以修改(被const
修饰的左值不可以修改)
右值代表赋值符号右侧的值,不可以直接取地址,一般为内置类型常量、函数返回值和表达式的值
右值不可以出现在赋值符号的左侧,否则会报错为不可修改的左值,但是左值可以出现在右侧,此时是将左值中的值赋值给赋值符号左侧的新左值,例如int b = 0; a = b;
右值可以分为两种
- 纯右值:一般为内置类型常量
- 将亡值:一般为函数返回值中的临时对象、匿名对象等即将被销毁的值
左值引用与右值引用
左值引用:即为对左值的引用,在类型后加一个&
即可代表左值引用类型,例如int num = 0; int& ref = num;
中ref
为左值num
的别名,表达式int& ref = num;
中的ref
和num
均为左值
右值引用:即为对右值的别名,在类型后加两个&
即可代表右值引用类型,例如int&& ref = 1;
,表达式中的ref
为对右值引用的左值,1为右值
一般情况下,左值引用的对象不可以是常量,因为临时变量具有常性,为了解决这个问题,可以使用const
修饰左值引用,例如const int& num = 1;
左值引用和右值引用的相互转化
左值引用可以通过强制转换转化为右值引用,也可以通过move()
函数进行变换,例如下面的代码
int main()
{
int num = 0;
// 左值引用
int& r1 = num;
// 将左值引用强制转换为右值引用
int&& r2 = (int&&)r1;
int&& r3 = move(r1);
return 0;
}
右值引用可以通过强制转换转化成左值引用,例如下面的代码
int main()
{
int num = 0;
// 左值引用
int& r1 = num;
// 将左值引用强制转换为右值引用
int&& r2 = (int&&)r1;
// 将右值引用强制转化为左值引用
int& r3 = (int&)r2;
return 0;
}
之所以可以通过强制转换实现左值引用和右值引用之间的相互转换是因为左值引用和右值引用在底层实际上都是一样的,只是语法层面对二者进行了更严格的定义,只要基本类型相同就可以通过强制转换进行改变,参考下图的汇编代码:
右值引用的使用
以模拟实现的string为例
namespace simulate_string
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(const string& s) -- 深拷贝" << endl;
if (this != &s)
{
_str[0] = '\0';
_size = 0;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0; // 不包含最后做标识的\0
};
simulate_string::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
simulate_string::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
拷贝构造函数与移动构造函数
在前面对于需要深拷贝的类来说,需要自己写类的拷贝构造函数,在没有移动构造函数时,只要是涉及到对象的拷贝都会调用拷贝构造函数,包括但不限于返回临时对象,例如下面的代码:
simulate_string::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
simulate_string::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
上面的代码中模拟实现了to_string
函数,函数返回一个simulate_string::string
类的对象str
,调用该函数:
int main()
{
simulate_string::string s = simulate_string::to_string(123);
return 0;
}
在编译器没有优化并且只有一个拷贝构造函数时,当执行main
函数中的第一条语句时,执行过程如下:
当编译器检测到当前写法simulate_string::string s = simulate_string::to_string(123);
时,会进行优化,所以执行过程优化为直接调用拷贝构造函数构造对象s
,如下图所示:
但是,进入拷贝构造函数中拷贝str
的内容会存在一定的空间和时间消耗,所以为了解决这个问题,可以采用移动拷贝构造,移动拷贝构造本质就是利用了右值引用。str
返回时,在未被编译器优化的情况下会生成一个临时对象,这个临时对象会进行一次拷贝,但因为临时对象是属于将亡值,所以使用右值引用的移动构造会更加方便且高效,只需要将临时对象中的值和现有对象中的值进行交换即可,移动构造如下:
// 移动构造
string(string&& s)
{
swap(s);
}
有了移动构造函数以后,对于需要使用返回的临时对象进行拷贝构造时就会直接走移动构造,从而减少原来拷贝构造的消耗
需要注意的是,为了确保可以交换成功形式参数不可以使用
const
修饰
在C++ 11中,构造函数也包括了移动构造,例如string类中的移动构造:
赋值重载函数与移动赋值重载函数
上面的移动构造只解决了在拷贝临时对象时会调用拷贝构造函数产生的消耗问题,如果main函数的代码修改为如下:
int main()
{
simulate_string::string s;
s = simulate_string::to_string(123);
return 0;
}
此时移动构造就无法解决问题,因为是赋值符号重载函数与临时对象之间的关系,同样,当编译器未进行优化时会进行下面的过程:
当编译器优化后,会直接调用一次赋值重载函数,用str
对象为s
对象赋值
但是尽管编译器进行了优化,赋值重载函数因为是将str
中的内容深拷贝给s
对象,所以依旧会产生开销,当对象很大时,开销也会变得很大。因为str
是将亡值,所以可以采用右值引用的方式,重载一个新的赋值重载函数如下,同样只需要交换一下将亡值和当前对象中的内容即可:
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
在C++ 11中,赋值重载函数也包括了移动赋值重载函数,例如string类中的移动赋值重载函数:
元素插入相关函数
以模拟实现的list类为例
#pragma once
#include <iostream>
#include <assert.h>
using namespace std;
namespace simulate_list
{
template<class T>
struct ListNode
{
ListNode<T>* _next;
ListNode<T>* _prev;
T _data;
ListNode(const T& data = T())
:_next(nullptr)
, _prev(nullptr)
, _data(data)
{}
};
template<class T, class Ref, class Ptr>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T, Ref, Ptr> Self;
Node* _node;
ListIterator(Node* node)
:_node(node)
{}
// ++it;
Self& operator++()
{
_node = _node->_next;
return *this;
}
Self& operator--()
{
_node = _node->_prev;
return *this;
}
Self operator++(int)
{
Self tmp(*this);
_node = _node->_next;
return tmp;
}
Self& operator--(int)
{
Self tmp(*this);
_node = _node->_prev;
return tmp;
}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& it)
{
return _node != it._node;
}
bool operator==(const Self& it)
{
return _node == it._node;
}
};
template<class T>
class list
{
typedef ListNode<T> Node;
public:
typedef ListIterator<T, T&, T*> iterator;
typedef ListIterator<T, const T&, const T*> const_iterator;
iterator begin()
{
return iterator(_head->_next);
}
const_iterator begin() const
{
return const_iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
const_iterator end() const
{
return const_iterator(_head);
}
void empty_init()
{
_head = new Node();
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
list(initializer_list<T> il)
{
empty_init();
for (const auto& e : il)
{
push_back(e);
}
}
// lt2(lt1)
list(const list<T>& lt)
{
empty_init();
for (const auto& e : lt)
{
push_back(e);
}
}
// lt1 = lt3
list<T>& operator=(list<T> lt)
{
swap(_head, lt._head);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
void push_back(const T& x)
{
insert(end(), x);
}
void pop_back()
{
erase(--end());
}
void push_front(const T& x)
{
insert(begin(), x);
}
void pop_front()
{
erase(begin());
}
// 没有iterator失效
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
// erase 后 pos失效了,pos指向节点被释放了
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
private:
Node* _head;
};
}
当在main函数中创建对象后进行尾插:
int main()
{
simulate_list::list<simulate_string::string> ls;
ls.push_back("11111");
return 0;
}
因为"1111"
属于常量字符串,属于右值,在push_back
函数中会调用Node
节点的构造函数初始化_data
,而因为_data
是simulate_string
类型的,所以会调用对应的构造函数将其转化为simulate_string
类型,接着再链接,但是整个过程会涉及到simulate_string
类的拷贝构造函数,并且因为_data
是左值,所以在进入simulate_string
类后也是左值,就不会调用前面的移动拷贝构造函数,这个现象也称为右值退化为左值,为了解决这个问题,首先需要修改push_back函数,将"11111"
识别为右值,所以需要使用右值引用作为push_back
函数的形式参数,所以push_back函数需要重载一份为如下形式:
void push_back(T&& x)
{
insert(end(), x);
}
接着,因为push_back
底层调用的还是insert
函数,所以insert
函数的形式参数也需要重载一份为如下形式:
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
但是,仅仅修改这两个函数的形式参数并不能解决问题,首先看push_back
函数,因为底层调用的是insert
函数,所以需要将push_back
的右值引用x
接收到的值继续传递给insert
函数,此时需要注意,右值引用本身还是左值,所以传递给insert
函数参数的x
依旧还是左值,此时尽管写了重载右值引用的insert
函数,依旧会调用左值引用的insert
函数,所以需要对push_back
函数进行进一步的修改,如下形式:
void push_back(T&& x)
{
insert(end(), move(x));// 将左值的右值引用转化为右值的右值引用
}
接着到insert
函数,在push_back
函数修改为上述形式后,此时调用的insert
函数即为重载右值引用的版本,接下来创建节点,此时依旧是同样的问题,x
是右值引用,但是本身是左值,所以传递给Node
节点的构造函数时也依旧是左值,所以同样需要将其转化为右值,如下形式:
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);// 将左值的右值引用转化为右值的右值引用
// ...
}
接着是Node节点的构造函数,当前情况下只有一个左值引用版本的构造函数,所以需要重载一个右值引用版本,如下形式:
ListNode(T&& data)
:_next(nullptr)
, _prev(nullptr)
, _data(data)
{}
但是上面的代码依旧是同样的问题,形式参数data是右值引用,但本身是左值,所以需要将data转化为右值,修改为:
ListNode(T&& data)
:_next(nullptr)
, _prev(nullptr)
, _data(move(data))
{}
此时再调用simulate_string
类的构造函数时,就会只调用移动拷贝构造函数
在C++ 11中,元素插入相关的函数也包括了右值引用的版本,例如list类中的右值引用版本的push_back
函数:
万能引用与完美转发
万能引用
万能引用可以将在函数模版中使用,形式如下:
// 万能引用
template<typename T>
void func(T&& t)
{
}
在上面的代码中,T&&
是一个万能引用,当传递左值时,T被推导为左值引用,当传递右值时,T被推导为右值引用,但是不论T
被推导为左值引用还是右值引用,形参t
本身依旧是左值,所以当需要再向下传递需要使用到右值时,依旧需要将t
进行转化,例如下面的例子:
// 查看引用类型
void Func(int& x)
{
cout << "左值引用" << endl;
}
void Func(int&& x)
{
cout << "右值引用" << endl;
}
// 万能引用
template<typename T>
void func(T&& t)
{
// 万能引用的类型推导
// T&&是一个万能引用,当传递左值时,T被推导为左值引用
// 当传递右值时,T被推导为右值引用
Func(t);
}
int main()
{
// 左值传递给func函数
int a = 10;
func(a);
// 右值传递给func函数
func(20);
// 将左值move为右值
func(move(a));
return 0;
}
输出结果:
左值引用
左值引用
左值引用
此时不论t
是左值引用还是右值引用,t
本身都是左值,所以传递给Func
函数只会走打印“左值引用”的部分,如果使用move
对形参t
进行转化,那么只会走打印“右值引用”的Func
函数,为了解决这个问题,可以使用完美转发
完美转发
完美转发可以将变量原有的类型传递给下一层,如果本身是左值引用,则完美转发后就是左值引用,如果本身是右值引用,则完美转发后就是右值引用,所以上面的代码可以写成:
// 查看引用类型
void Func(int& x)
{
cout << "左值引用" << endl;
}
void Func(int&& x)
{
cout << "右值引用" << endl;
}
// 万能引用
template<typename T>
void func(T&& t)
{
// 使用完美转发
Func(forward<T>(t));
}
int main()
{
// 左值传递给func函数
int a = 10;
func(a);
// 右值传递给func函数
func(20);
// 将左值move为右值
func(move(a));
return 0;
}
输出结果:
左值引用
右值引用
右值引用
有了完美转发后,就可以对上面list模拟实现中的插入函数进行修改,前面遇到的问题就是本身是右值,给了右值引用再向下传递时发生右值引用退化为左值引用,所以可以使用完美转发使其按照右值引用的方式传递,以push_back
为例
void push_back(T&& x)
{
insert(end(), forward<T>(x));// 将左值的右值引用转化为右值的右值引用
}
C++ 11新增的两个默认成员函数
在有了移动拷贝构造函数和移动赋值重载函数后,类的默认成员函数从原有的6个变为了现在的8个,对于新增的两个默认成员函数来说,默认的生成规则如下:
- 移动拷贝构造函数:当类中没有显式写移动拷贝构造函数并且类中也没有显式写拷贝构造函数、析构函数和赋值重载函数时,编译器会默认生成移动拷贝构造函数,该函数的行为是,当类中的成员是内置类型时,将会进行值拷贝,当类中的成员是自定义类型时,会调用自定义类型的移动拷贝构造函数,如果自定义类型也没有移动拷贝构造函数,则调用拷贝构造函数
- 移动赋值重载函数:当类中没有显式写移动赋值重载函数并且类中也没有显式写拷贝构造函数、析构函数和赋值重载函数时,编译器会默认生成移动赋值重载函数,该函数的行为是,当类中的成员是内置类型时,将会进行值拷贝,当类中的成员是自定义类型时,会调用自定义类型的移动赋值重载函数,如果自定义类型也没有移动赋值重载函数,则调用赋值重载函数
之所以需要满足两个条件(1. 没有显式写移动拷贝构造函数 2. 没有显式写拷贝构造函数、析构函数和赋值重载函数)可以考虑移动赋值重载函数和移动拷贝构造函数的使用场景:当类中的成员都是内置类型时,没有大型资源释放行为,所以不需要显示写析构函数,此时对于拷贝构造函数和赋值重载函数来说,直接复制原有对象的内容开销也不大,而对于移动拷贝构造函数和移动赋值重载函数来说,目的就是解决拷贝构造函数和赋值重载函数在部分场景下的时间和空间消耗,所以没有拷贝构造函数、析构函数和赋值重载函数代表没有大型的资源释放行为,自然也就可以不会产生大量的时间和空间消耗,所以也就不需要写移动拷贝构造函数和移动赋值重载函数
生成默认成员函数=default
如果一定要生成默认的移动拷贝构造和移动赋值重载函数时,可以使用=default
关键字,例如如果需要默认生成string类的拷贝构造函数,可以写成:
string(const string& s)=default;
需要注意的是,如果想让编译器默认生成移动拷贝构造函数和移动赋值重载函数时,一定要有拷贝构造函数、析构函数和赋值重载函数的出现,哪怕是使用
=default
让移动拷贝构造函数和移动赋值重载函数默认生成,不可以缺少三个默认成员函数的一个,否则无法通过编译
不生成默认成员函数=delete
如果不想编译器默认生成某一个默认成员函数时,可以使用=delete
关键字,例如如果不像默认生成string类的拷贝构造函数,可以写成:
string(const string& s)=delete;
在C++ 98中,如果不想一个对象可以通过调用拷贝构造函数和赋值重载函数进行构造可以将对应的拷贝构造函数和赋值重载函数修饰为private
,而在有了C++ 11的=delete
关键字后,就可以对这两个函数修饰为=delete
,而无需在放入private
中
C++ 11中的可变参数模版
可变参数模版介绍
在C++ 98中,如果一个函数是一个模版函数,那么该函数可以传递的参数个数就由模版参数个数决定,如果传递参数多于或少于(此处不考虑含有缺省参数的情况)规定的模版个数,则编译器无法生成对应的函数
在C++ 11中,为了解决上面的问题提出了可变参数的函数模版,基本格式如下:
// 可变参数模版
template<class... Args>
void func(Args... args)
{
}
在上面的代码中使用...
代表可变参数,...Args
代表模版参数包,... args
代表形式参数包,参数包中可以有[0, N](N >= 0且N为整数)个模版参数,此时编译器会根据传递的参数个数生成对应的函数。
如果想要获取函数参数的数量时,可以使用sizeof
运算符计算形式参数包,代码如下:
// 可变参数模版
template<class... Args>
void func(Args... args)
{
cout << sizeof...(args) << endl;
}
sizeof
计算属于编译时就可以计算的,所以可以直接使用,需要注意省略号的所在位置
如果想在函数func
中查看传递的参数时则不可以使用遍历等运行时的逻辑进行打印,例如使用for
循环
template<class... Args>
void func(Args... args)
{
//cout << sizeof...(args) << endl;
for (size_t i = 0; i < sizeof...(args); i++)
{
cout << args[i] << endl;
}
}
报错信息:
'args': parameter pack must be expanded in this context
参数包展开
为了能够展示参数,下面采用两种方法进行显示,以下面的代码为例:
int main()
{
// 可变参数模版
func();
func(1, 2, 3);
func(1, "hello", 3.14);
return 0;
}
- 编译时递归展开参数包
编译时递归和运行时递归的最大区别就是不可以使用if
语句进行递归结束条件的判断
编译时递归展开参数包的思路是:创建一个有可变参数模版的函数func
,该模版参数含有两部分,第一个部分是万能引用的单一参数,该部分用于接收每一个参数值,第二个部分是可变参数模版,在函数体内,先打印第一个参数的内容,再调用func
函数,传递剩余的参数,用于递归调用,再外侧写一个无参的func
函数,该函数作为编译时递归的终止条件
// 递归终止函数
void func()
{
cout << endl;
}
template<class T, class... Args>
void func(T&& x, Args... args)
{
// 打印当前的x
cout << x << " ";
// 递归调用打印剩余的参数
func(forward<Args>(args)...);
}
对于上面的测试函数,结果如下:
1 2 3
1 hello 3.14
以第二个测试为例func(1, 2, 3);
,上面的代码可以理解为:
//4. 第四步
void func()
{
cout << endl;
}
//3. 第三步
void func(int&& z)
{
cout << z << " ";
func();
}
//2. 第二步
void func(int&& y, int&& z)
{
cout << y << " ";
func(forward<int>(z));
}
//1. 第一步
void func(int&& x, int&& y, int&& z)
{
// 打印当前的x
cout << x << " ";
// 递归调用打印剩余的参数
func(forward<int>(y), forward<int>(z));
}
- 利用数组根据个数初始化数组大小的机制展开参数包
该方式的原理是:当一个数组在初始化时,如果不指定数组的大小,编译器会根据数组的元素个数推导出数组的大小,所以可以写为:
template <class... Args>
void func(Args&&... args)
{
int arr[] = { ((cout << forward<Args>(args) << " ", 0))... };
cout << endl;
}
这个方法需要注意,对于没有实参的函数来说会编译报错,例如func()
函数,并且不可以去掉逗号表达式,因为cout
的返回值是ostream
类型,该类型不支持赋值运算符重载函数
上面的方法可以理解为:
// 上面的展开可以理解为
void func(int&& x, int&& y, int&& z)
{
int arr[] = { ((cout << forward<int>(x) << " ", 0)), ((cout << forward<int>(y) << " ", 0)), ((cout << forward<int>(z) << " ", 0)) };
cout << endl;
}
因为数组在开辟大小是会计算元素的个数,逗号表达式的左右两侧都会进行运算,所以先打印x的值,再执行0,所以第一个表达式的值为0,作为数组的第一个元素,以此类推直到最后一个元素
可变参数模版的应用emplace
系列函数
在前面学习到的容器中,基本上都支持emplace
系列的函数,以list类为例,list类中存在一个emplace_back
函数,该函数可以支持在list的尾部插入数据,与push_back
函数实现的功能基本一致,但是emplace_back
函数除了可以支持插入已经构造的对象和单个用于构造对象的值,还可以接受构造函数的参数,直接在list的内存空间中调用该对象的构造函数,避免了额外的拷贝或移动操作。这可以提高效率,尤其是在处理复杂杂对象时,例如下面的代码:
int main()
{
// 插入一个新的对象
list<string> ls;
string s1("1111111");
ls.push_back(s1);
ls.emplace_back(s1);
// 插入一个右值
ls.push_back("2222222");
ls.emplace_back("2222222222");
// 插入时构造对象
list<pair<simulate_string::string, int>> ls1;
// 插入时,用插入的内容构造一个pair对象
ls.emplace_back("2222222", 2);
return 0;
}
之所以可以接受构造函数的参数,是因为emplace_back
函数本身是一个可变模版参数的函数模版,但是注意,这个可变参数模版不代表可以传递多个参数,例如,插入多个字符串ls.emplace_back("1111", "2222");
这种行为是错误的
使用可变参数模版模拟实现emplace_back
函数,以模拟实现list为例:
因为emplace_back
本身是一个插入函数,所以底层调用insert
函数即可,将函数的形式参数设置为右值引用,为了可以实现向下传递时也是右值引用,需要使用完美转发,代码如下:
// 模拟实现emplace_back
template<class... Args>
void emplace_back(Args&&... args)
{
insert(end(), T(forward<Args>(args)...));
}
接着,实现insert
函数针对emplace_back
的版本,因为需要调用构造函数,所以当是右值引用时,需要保留是右值引用,同样需要使用完美转发
template<class... Args>
void insert(iterator pos, Args.., args)
{
Node* cur = pos._node;
Node* newnode = new Node(forward<Args>(x));// 保证右值引用不退化
// ...
}
最后,完善Node
节点的构造函数,使其满足可变参数模版,同样需要完美转发
template <class... Args>
ListNode(Args... args)
: _next(nullptr)
, _prev(nullptr)
, _data(forward<Args>(args)...) // 保证右值引用不退化
{}
emplace
系列函数和push
系列函数的选择
以vector中的emplace_back
和push_back
为例
push_back
和 emplace_back
都是 vector 类的成员函数,用于在 vector 的末尾添加元素。它们之间的主要区别在于添加元素的方式:
push_back
:接受一个已存在的对象作为参数,进行拷贝或移动,将其添加到 vector 的未尾。这会引发一次拷贝或移动构造函数的调用,具体取决于传递的对象是否可移动。emplace_back
:接受构造函数的参数,直接在 vector 的内存空间中调用该对象的构造函数,避免了额外的拷贝或移动操作。这可以提高效率,尤其是在处理复杂对象时。
使用场景:
- 如果需要将一个已经存在的对象添加到vector中,使用
push_back
- 如果希望直接在vector中构造对象,避免额外的拷贝或移动开销,使用
emplace_back
当然可以无脑选择
emplace_back
函数