目录
1、右值引用和移动语义
C++11之后中新增了的右值引用语法特性,C++11之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
1.1 左值和右值
左值是一个表示数据的表达式(如变量名或解引用的指针等),一般是有持久状态,存储在内存中,可修改,我们可以获取它的地址,左值可以出现在 " = " 的左边或右边。定义时const修饰后的左值,不能修改,但是可以取它的地址。
右值也是一个表示数据的表达式(如字面值常量或临时对象等),一般没有名称、没有内存地址,不可修改,右值不能取地址,右值只能出现在 " = " 的右边。
左值(lvalue)传统解释是 left value,C++11之后,更准确地解释为 locator value (定位值,可以取地址)
右值(rvalue)传统解释是 right value,C++11之后,更准确地解释为 read value (可读值,不可取地址)
#include<iostream>
using namespace std;
int main()
{
// 左值:可以取地址
// 以下的 p、b、c、*p、s、s[0] 就是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
cout << &c << endl;
cout << (void*)&s[0] << endl; // cout打印char*是打印字符串,转成void*才能打印地址
// 右值:不能取地址
// 以下几个 10、x + y、fmin(x, y)、string("11111") 都是常见的右值
double x = 1.1, y = 2.2;
10;
x + y;
fmin(x, y);
string("11111");
// cout << &10 << endl;
// cout << &(x+y) << endl;
// cout << &(fmin(x, y)) << endl;
// cout << &string("11111") << endl;
return 0;
}
1.2 左值引用和右值引用
Type& r1 = x; 就是左值引用,给左值取别名;
Type&& rr1 = y; 就是右值引用,给右值取别名。
左值引用不能直接引用右值,但是const左值引用可以引用右值。
右值引用不能直接引用左值,但是右值引用可以引用move(左值)。
template <class _Ty>
remove_reference_t<_Ty>&& move(_Ty&& _Arg)
{
// forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}
move是库里面的一个函数模板,本质内部是进行强制类型转换(左值->右值),标记为可窃取资源的对象,也可以move(右值),还是右值(增强可读性),当然还涉及一些引用折叠的知识,这个后面会细讲。
注意:变量表达式都是左值属性,那么,左值引用和右值引用本身是左值,可以被修改,如:上面的r1是左值引用,rr1是右值引用,但本身都是左值,可以被修改,那么右值(一般不可修改)就可以通过右值引用进行修改,就可以达到窃取资源的目的。
从语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中r1和rr1的汇编层实现,底层都是用指针实现的,所以左值->右值。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要混到一起理解,互相佐证反而会陷入迷途。
#include<iostream>
using namespace std;
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常见的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
double x = 1.1, y = 2.2;
// 左值引用给左值取别名
int& r1 = b;
int*& r2 = p;
int& r3 = *p;
string& r4 = s;
char& r5 = s[0];
// 右值引用给右值取别名
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
// 左值引用不能直接引用右值,但是const左值引用可以引用右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
// 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
int&& rrx1 = move(b);
int*&& rrx2 = move(p);
int&& rrx3 = move(*p);
string&& rrx4 = move(s);
string&& rrx5 = (string&&)s; // 强制类型转化
// b、r1、rr1都是变量表达式,都是左值
cout << &b << endl;
cout << &r1 << endl;
cout << &rr1 << endl;
int& r6 = r1;
// 这里要注意的是,rr1的属性是左值,所以不能再被右值引用绑定,除非move一下
// int&& rrx6 = rr1;
int&& rrx6 = move(rr1);
return 0;
}
1.3 引用延长生命周期
注意:引用延长生命周期,只能延长在当前作用域的生命周期。
右值引用可用于延长临时对象的生命周期,但右值引用本身是左值,可以被修改。
const的左值引用也能延长临时对象的生命周期,但这些对象无法被修改。
#include <iostream>
#include <string>
int main()
{
std::string s1 = "Test";
const std::string& r2 = s1 + s1; // OK:const左值引用可延长临时对象生命周期
// r2 += "Test"; // 错误:不能通过const引用修改
std::string&& r3 = s1 + s1; // OK:右值引用延长临时对象生存期
r3 += "Test"; // OK:可通过非const右值引用修改
std::cout << r3 << '\n';
return 0;
}
1.4 左值和右值的参数匹配
C++98中,我们实现一个const左值引用作为形参的函数,那么实参传递左值和右值都可以匹配。
C++11以后,分别重载,左值引用、const左值引用、右值引用作为形参的 f 函数,会调用最匹配的。
#include <iostream>
using namespace std;
void f(int& x) {
cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x) {
cout << "const左值引用重载 f(" << x << ")\n";
}
void f(int&& x) {
cout << "右值引用重载 f(" << x << ")\n";
}
int main() {
int i = 1;
const int ci = 2;
f(i); // 调用 f(int&)
f(ci); // 调用 f(const int&)
f(3); // 调用 f(int&&),若无此重载则调用 f(const int&)
f(std::move(i)); // 调用 f(int&&)
// 右值引用变量在表达式中是左值
int&& x = 1;
f(x); // 调用 f(int&)
f(std::move(x)); // 调用 f(int&&)
return 0;
}
1.5 右值引用和移动语义的使用场景
1.5.1 左值引用主要使用场景
左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。
但是有些场景不能使用传左值引用返回,如 addStrings 和 generate 函数,C++98 中的解决方案只能是被迫使用输出型参数解决。那么 C++11 以后这里可以使用右值引用做返回值解决吗?显然是不能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回,只能延长对象在当前函数栈帧的生命周期,但函数栈帧已经销毁了,对象会析构,无力回天了。
class Solution {
public:
// 传值返回需要拷贝
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size() - 1;
int end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0) {
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1) {
str += '1';
}
reverse(str.begin(), str.end());
return str;
}
// 这里的传值返回拷贝代价就太大了
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
return vv;
}
};
1.5.2 移动构造和移动赋值
移动构造是一种构造函数,类似拷贝构造函数,
移动构造函数要求第一个参数是该类类型对象的右值引用,后面只能加缺省参数。
移动赋值是一个赋值运算符的重载,类似拷贝赋值函数,
移动赋值函数要求第一个参数是该类类型对象的右值引用。
对于像 string/vector 这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第一个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源(右值对象一般是临时对象(返回的局部对象也认为是临时对象),直接swap临时对象的资源,不走深拷贝),从提高效率。
注意:
1. move(左值)是让左值->右值,本身没有窃取资源,是移动构造和移动赋值窃取(swap)资源。
2. 对于内置类型,只需赋值就行,就算是移动,窃取的本质也是赋值,所以不需要移动语义。
3. 个人疑惑,移动构造为什么不叫移动拷贝构造,不是用右值对象来构造的吗?因为拷贝,有不改变原对象的意思,避免混淆。
下面的 Lzc::string 样例实现了移动构造和移动赋值,我们需要结合场景理解。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <assert.h>
#include <string.h>
#include <algorithm>
using namespace std;
namespace Lzc
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin() { return _str; }
iterator end() { return _str + _size; }
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);
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
{
cout << "string(const string& s) -- 拷贝构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
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& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
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)
{
push_back(ch);
return *this;
}
const char* c_str() const { return _str; }
size_t size() const { return _size; }
private:
char* _str = new char('\0');
size_t _size = 0;
size_t _capacity = 0;
};
}
int main()
{
// 构造
Lzc::string s1("xxxxx");
// 拷贝构造
Lzc::string s2 = s1;
// 构造+移动构造,优化后直接构造
Lzc::string s3 = Lzc::string("yyyyy");
// 移动构造
Lzc::string s4 = move(s1);
cout << "******************************" << endl;
return 0;
}
1.5.3 右值引用和移动语义解决传值返回问题
namespace Lzc {
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
int next = 0;
while (end1 >= 0 || end2 >= 0) {
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
cout << "******************************" << endl;
return str;
}
}
int main() {
Lzc::string ret;
ret = Lzc::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
g++ -std=c++11 test.cpp -fno-elide-constructors -o test && ./test
使用C++11标准,去掉编译器优化,编译为test,并执行。
1.5.4 右值引用和移动语义在传参中的提效
查看STL文档我们发现,C++11以后容器的push系列和insert系列的接口都增加了右值引用版本。
当实参是一个左值时,左值引用,容器内部继续调用拷贝构造进行拷贝,将拷贝的对象放到容器空间中。
当实参是一个右值时,右值引用,容器内部则调用移动构造(由于右值引用本身是左值,会走拷贝构造,需move,转成右值,走移动构造),将窃取了临时对象的资源的对象放到容器空间中。
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node(const T& data = T())
:_data(data) // 拷贝构造
,_next(nullptr)
,_prev(nullptr)
{}
list_node(T&& data)
:_data(move(data))// 移动构造
, _next(nullptr)
, _prev(nullptr)
{}
};
其实这里还有一个emplace系列的接口,但是这个涉及可变参数模板,我们需要把可变参数模板讲解以后再讲解emplace系列的接口。
总结一下:
编译器优化,不是C++的标准,取决于编译器。
右值引用+移动语义 与 编译器优化,差别不大,有时略胜一筹,只需支持C++11的右值引用和移动语义,不依赖编译器。
但是,右值引用+移动语义 与 编译器优化 = 完美。
多嘴一句:因为之前的C++委员会有点"摆烂",没有出右值引用和移动语义,所以编译器自己优化,所以现在理解有点难受。
1.6 类型分类(了解)
英文:Value categories - cppreference.com
一般看,左值,右值。
左值(lvalue) 是指具有持久性、有名字的表达式,可以取地址,通常代表一个对象的内存位置。
纯右值(prvalue) 是临时对象,通常是计算过程中产生的中间结果,没有名字,不能取地址。
将亡值(xvalue) 是即将被move的对象,通常是“可以被窃取资源”的右值。
1.7 引用折叠
1.7.1 语义折叠的概念
1. C++中不能直接定义引用的引用,如 int& && r = i; 这样写会直接报错。
2. 模板或typedef中的类型操作可以构成引用的引用。 这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
#include <iostream>
// 由于引用折叠限定,f1 实例化以后总是一个左值引用
template<class T>
void f1(T& x) {}
// 由于引用折叠限定,f2 实例化后可以是左值引用,也可以是右值引用
// 也称: 万能引用
template<class T>
void f2(T&& x) {}
int main() {
typedef int& lref;
typedef int&& rref;
int n = 0;
// 引用折叠示例
lref& r1 = n; // r1 的类型是 int&
lref&& r2 = n; // r2 的类型是 int&
rref& r3 = n; // r3 的类型是 int&
rref&& r4 = 1; // r4 的类型是 int&&
// f1 函数模板实例化与调用情况
// 没有折叠 -> 实例化为 void f1(int& x)
f1<int>(n);
// f1<int>(0); // 报错,不能将右值绑定到左值引用
// 折叠 -> 实例化为 void f1(int& x)
f1<int&>(n);
// f1<int&>(0); // 报错,不能将右值绑定到左值引用
// 折叠 -> 实例化为 void f1(int& x)
f1<int&&>(n);
// f1<int&&>(0); // 报错,不能将右值绑定到左值引用
// 折叠 -> 实例化为 void f1(const int& x)
f1<const int&>(n);
f1<const int&>(0);
// 折叠 -> 实例化为 void f1(const int& x)
f1<const int&&>(n);
f1<const int&&>(0);
// f2 函数模板实例化与调用情况
// 没有折叠 -> 实例化为 void f2(int&& x)
// f2<int>(n); // 报错,不能将左值绑定到右值引用
f2<int>(0);
// 折叠 -> 实例化为 void f2(int& x)
f2<int&>(n);
// f2<int&>(0); // 报错,不能将右值绑定到左值引用
// 折叠 -> 实例化为 void f2(int&& x)
// f2<int&&>(n); // 报错,不能将左值绑定到右值引用
f2<int&&>(0);
return 0;
}
个人疑惑:
const int&& x,x是右值引用,右值一般不可修改,因为x本身是左值,所以可以修改,如果还加const,那就是右值不可修改,这用说吗?而且只可以接受右值。
我const int& x,也是不可修改,还可以接受左值和右值。
但是一般使用函数模板,不显示实例化,
那么万能引用函数模板,的推导过程是:
传右值,T就是右值的类型,T&&就是右值引用,
传左值,T就是左值类型的左值引用,T&&就是左值引用。
#include <iostream>
#include <utility> // move
template<class T>
void Function(T&& t) {
int a = 0;
T x = a;
// x++;
std::cout << &a << std::endl;
std::cout << &x << std::endl << std::endl;
}
int main() {
// 10 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)
Function(10);
int a;
// a 是左值,推导出 T 为 int&,引用折叠,模板实例化为 void Function(int& t)
Function(a);
// std::move(a) 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)
Function(std::move(a));
const int b = 8;
// b 是 const 左值,推导出 T 为 const int&,引用折叠,模板实例化为 void Function(const int& t)
// Function 内部会编译报错,因为 x 不能 ++
Function(b);
// std::move(b) 是 const 右值,推导出 T 为 const int,模板实例化为 void Function(const int&& t)
// 所以 Function 内部会编译报错,x 不能 ++
Function(std::move(b));
return 0;
}
1.7.2 引用折叠的应用
左值引用和右值引用的函数,只有参数部分不同,函数体基本相同,高度相似->模板。
例:
template<class T> list{ };中的push_back,此时T&&不是万能引用,因为list模板实例化了,T就已经确定了。要再加一层模板,才能构成万能引用。
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), move(x)); // x本身是左值
}
那么list模板里面再来个函数模板,这个时候万能引用就体现出价值了,
传左值就实例化左值引用版本,传右值就实例化右值引用版本。
但是有一个问题,不能直接move(x),因为可能是左值引用版本,若move(x),原对象的资源可能被窃取,改变了原对象。
左值 → 会调用拷贝语义(保留原对象)。
右值 → 会调用移动语义(允许“窃取”资源)。
// 万能引用
template<class X>
void push_back(X&& x)
{
insert(end(), x);
}
这个时候就需要完美转发了,右值引用(本身是左值)返回右值引用,左值引用返回左值引用,然后再传。
其实,这个逻辑没错,但是例子有点小瑕疵,如果是list<pair<int,int>>,没有万能引用,一开始就确定了T是pair<int,int>,可以push_back({1,2}),走类型转换,但是如果写成万能引用,就不能push_back({1,2}),因为在传参时,确定类型,不知道{1,2}是什么类型。
1.8 完美转发
// 左值版本(T 是具体类型,如 int&)
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
// 右值版本(T 是具体类型,如 int&&)
template<typename T>
constexpr T&& forward(typename std::remove_reference<T>::type&& t) noexcept {
static_assert(!std::is_lvalue_reference<T>::value, "Cannot forward rvalue as lvalue");
return static_cast<T&&>(t);
}
std::remove_reference_t<T>&
和 typename std::remove_reference<T>::type&
在功能上是完全等价的
形式 | 说明 | 引入标准 |
---|---|---|
typename std::remove_reference<T>::type& |
传统的 traits 用法,需要 typename 关键字 |
C++98/03 |
std::remove_reference_t<T>& |
C++14 引入的简化写法,_t 后缀表示直接取类型 |
C++14 |
remove_reference_t<T>:T变成非引用类型(去掉了传过来的T中的&)。
static_cast<T&&>(x):T&&会引用折叠,x强制转成T&&类型。
当 T
是非引用类型时(即原始参数是右值),选择过程如下:
情况1:传入 右值(如 std::forward<int>(10)
)
模板参数
T
被推导为int
(非引用类型)匹配过程:
左值版本参数:
remove_reference_t<int>&
→int&
❌ 无法绑定到右值10
右值版本参数:
remove_reference_t<int>&&
→int&&
✅ 精确匹配右值
选择右值版本
static_cast<T&&>
→ 生成右值引用类型的表达式
编译器魔法:这个表达式会被特殊标记为"可以匹配右值引用参数"。
情况2:传入 左值(如 int x; std::forward<int&>(x)
)
模板参数
T
被推导为int&
(左值引用)匹配过程:
左值版本参数:
remove_reference_t<int&>&
→int&
✅ 精确匹配左值右值版本参数:
remove_reference_t<int&>&&
→int&&
❌ 无法绑定左值
选择左值版本
static_cast<T&&>
→ 生成左值引用类型的表达式
所以:
// 万能引用
template<class X>
void push_back(X&& x)
{
insert(end(), forward<T>(x));
}
注意:万能引用进行传参时,通常需要完美转发。
2、可变参数模板
2.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...运算符去计算参数包中参数的个数。
template <class ...Args>
void Print(Args&&... args) {
cout << sizeof...(args) << endl;
}
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:更本质去看没有可变参数模板,我们实现出这样的多个函数模板才能支持
// 这里的功能,有了可变参数模板,我们进一步被解放,他是类型泛化基础
// 上叠加数量变化,让我们泛型编程更灵活。
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);
// ...
2.2 包扩展
对于一个参数包,我们除了能计算它的参数个数,我们能做的唯一的事情就是扩展它。
// 可变模板参数
// 参数类型可变
// 参数个数可变
// 打印参数包内容
// template <class... Args>
// void Print(Args... args)
// {
// 可变参数模板编译时解析
//
// 下面是运行获取和解析,所以不支持这样用
// cout << sizeof...(args) << endl;
// for (size_t i = 0; i < sizeof...(args); i++)
// {
// cout << args[i] << " "; // 不支持这样用
// }
// cout << endl;
// }
2.2.1 直接扩展
template<class... Args>
void Print(Args... args) {
// 完全展开:生成与args数量相同的参数
SomeFunc(args...);
}
Print(1, "abc", 2.0);
// 展开为:SomeFunc(1, "abc", 2.0);
2.2.2 递归扩展
模板是写给编译器的。
#include <iostream>
#include <string>
using namespace std;
void ShowList() {
// 编译器时递归的终止条件,参数包是0个时,直接匹配这个函数
cout << endl;
}
// 传过来的args,是N个参数的参数包
// 调用ShowList,参数包的第一个传给x,剩下N-1传给第二个参数包
template <class T, class... Args>
void ShowList(T x, Args... args) {
cout << x << " ";
ShowList(args...); // 触发扩展操作
}
// 编译时递归推导解析参数
template <class... Args>
void Print(Args... args) {
ShowList(args...); // 触发扩展操作
}
int main() {
Print(1, string("xxxxx"), 2.2);
return 0;
}
// Print(1, string("xxxxx"), 2.2);调用时
// 本质编译器将可变参数模板通过模式的包扩展,编译器推导的以下三个重载函数函数
// void ShowList(double x)
// {
// cout << x << " ";
// ShowList();
// }
//
// void ShowList(string x, double z)
// {
// cout << x << " ";
// 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);
// }
2.2.3 函数调用式包扩展
和直接扩展相比,可以对每个参数预处理。
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) {
// 注意GetArg必须返回接收到的对象,这样才能组成参数包给Arguments
Arguments(GetArg(args)...);
}
// void Print(int x, string y, double z)
// {
// }
// 本质可以理解为编译器编译时,包的扩展模式
// 将上面的函数模板扩展实例化为下面的函数
// 是不是很抽象,C++11以后,只能说委员会的大佬设计语法思维跳跃得太厉害
// Arguments(GetArg(x), GetArg(y), GetArg(z));
int main() {
Print(1, string("xxxxx"), 2.2);
return 0;
}
2.3 emplace系列接口
template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position
, Args&&... args);
C++11 以后 STL 容器新增了 emplace 系列的接口,emplace 系列的接口均为可变参数模板,功能上兼容 push 和 insert 系列。假设容器为 container<T>,emplace 还支持直接插入构造 T 对象的参数,可以直接在容器空间上构造 T 对象,高效一些。
下面我们模拟实现了 list 的 emplace 和 emplace_back 接口,这里把参数包不断往下传递,最终在节点的构造中直接去匹配容器存储的数据类型 T 的构造,可以直接在容器空间上构造 T 对象。
传递参数包过程中,如果是Args&&... args的万能引用参数包,要用完美转发参数包,方式如下
std::forward<Args>(args)...,std::forward
分别应用到 args
中的每一个参数上。否则编译时包扩展后右值引用变量表达式就变成了左值。
emplace直接在容器内构造对象,避免临时对象的创建和拷贝/移动,因此在多数情况下比push/insert更高效。
如:下面的push_back和insert没有实现万能引用,对于list<pair<int,int>>,push_back({1,2})可以类型转换,万能引用就不能类型转换(不知道{1,2}是什么类型),更灵活。
#pragma once
#include<assert.h>
namespace Lzc
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node() = default;
template <class... Args>
list_node(Args&&... args)
: _next(nullptr)
, _prev(nullptr)
, _data(std::forward<Args>(args)...)
{
}
};
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;
}
Ptr operator->()
{
return &_node->_data;
}
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;
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._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 _head->_next;
}
iterator end()
{
return _head;
}
const_iterator begin() const
{
return _head->_next;
}
const_iterator end() const
{
return _head;
}
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
_size = 0;
}
list()
{
empty_init();
}
list(initializer_list<T> il)
{
empty_init();
for (auto& e : il)
{
push_back(e);
}
}
// lt2(lt1)
list(const list<T>& lt)
{
empty_init();
for (auto& e : lt)
{
push_back(e);
}
}
// lt1 = lt3
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
auto it = begin();
while (it != end())
{
it = erase(it);
}
}
void swap(list<T>& lt)
{
std::swap(_head, lt._head);
std::swap(_size, lt._size);
}
void push_back(const T& x)
{
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), forward<T>(x));
}
// 万能引用
/*template<class X>
void push_back(X&& x)
{
insert(end(), forward<X>(x));
}*/
template <class... Args>
void emplace_back(Args&&... args)
{
insert(end(), std::forward<Args>(args)...);
}
void push_front(const T& x)
{
insert(begin(), x);
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
// prev newnode cur
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
++_size;
return newnode;
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(forward<T>(x));
// prev newnode cur
newnode->_next = cur;
cur->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
++_size;
return newnode;
}
// 万能引用
//template<class X>
//iterator insert(iterator pos, X&& x)
//{
// Node* cur = pos._node;
// Node* prev = cur->_prev;
// Node* newnode = new Node(forward<X>(x));
// // prev newnode cur
// newnode->_next = cur;
// cur->_prev = newnode;
// newnode->_prev = prev;
// prev->_next = newnode;
// ++_size;
// return newnode;
//}
template <class... Args>
iterator insert(iterator pos, Args&&... args)
{
Node* cur = pos._node;
Node* newnode = new Node(std::forward<Args>(args)...);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* prev = pos._node->_prev;
Node* next = pos._node->_next;
prev->_next = next;
next->_prev = prev;
delete pos._node;
--_size;
return next;
}
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
private:
Node* _head;
size_t _size;
};
}
3、类的新功能
3.1 默认的移动构造和移动赋值
原来 C++ 类中,有 6 个默认成员函数:构造函数 / 析构函数 / 拷贝构造函数 / 拷贝赋值重载 / 取地址重载 /const 取地址重载,最重要的是前 4 个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果你自己没有实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一
个(因为这几个是绑定到一起的,都不写,说明默认生成的就够用了)。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执
行逐成员按字节拷贝(浅拷贝),自定义类型成员,如果实现了移动构造就调用移动构造,没有实现就调用拷贝构造。
如果你自己没有实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意
一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置类型成员会
执行逐成员按字节拷贝(浅拷贝),自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值与移动构造完全类似)
如果你自己实现了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
3.2 成员变量声明时给缺省值
3.3 default和delete
C++11 可以让你更好地控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成,可以使用 default 关键字显式指定生成,比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用 default 关键字显式指定移动构造生成。
C++11如果想要限制某些默认函数的生成,只需在该函数声明加上 = 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:
bit::string _name;
int _age;
};
int main() {
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}