C++11 列表初始化、右值引用、移动语义、引用折叠与完美转发
- 一、列表初始化
- C++98 传统的 {}
- C++11 中的 {}
- C++11 中的 std::initializer_list
- C++11 {} 列表初始化 与 std::initializer_list 区别
- 二、右值引用
- 左值和右值
- 左值引用和右值引用
- 引用延长生命周期
- 左值和右值的参数匹配
- 类型分类
- 三、移动语义
- 移动构造和移动赋值
- 右值引用与移动语义的关系
- 右值引用和移动语义解决传值返回问题
- 右值引用和移动语义在传参中的提效
- 四、引用折叠
- 万能引用
- 万能引用推导细节
- 五、完美转发
以下代码环境为 VS2022 C++。
一、列表初始化
C++98 传统的 {}
C++98中一般数组和结构体可以用 {} 进行初始化
#include <iostream>
using namespace std;
struct one
{
int a;
int b;
};
int main()
{
int arr1[] = { 1, 2, 3, 4, 5 };
int arr2[10] = { 0 };
one get1 = { 1, 2 };
return 0;
}
C++11 中的 {}
C++11以后想统一初始化方式,试图实现一切对象皆可用 {} 初始化,{} 初始化也叫做列表初始化。
内置类型支持,自定义类型也支持,自定义类型本质是类型转换,中间会产生临时对象,如果编译器有优化会变成直接构造。
{} 初始化的过程中,可以省略掉 “ = ”。
C++11列表初始化的本意是想实现一个大统一的初始化方式,其次它在有些场景下带来的不少便利,如容器 push / inset 多参数构造的对象时,{} 初始化会很方便。
#include <iostream>
#include <vector>
using namespace std;
struct one
{
int a;
int b;
};
class two
{
int a;
int b;
int c;
public:
two(int ta, int tb, int tc)
:a(ta)
, b(tb)
,c(tc)
{
;
}
two(int ta, int tb = 20)
:a(ta)
,b(tb)
{
;
}
};
int main()
{
// C++98
int arr1[] = { 1, 2, 3, 4, 5 };
int arr2[10] = { 0 };
one get1 = { 1, 2 };
// 注意 C++98 支持单参数时类型转换,也可以不用 {}
two get5 = { 20 };
two get4 = 20;
string str = "hahaha";
// C++11
// 内置类型支持 {}
int num = { 0 };
// 自定义类型支持 {}
// 理论上会走 有参构造 + 下面介绍的移动构造,
// 如果编译器有优化会直接 有参构造
two get2 = { 1, 2, 3 };
// 用 const 引用来引用 有参构造 的临时对象
const two& get3 = { 3, 2, 1 };
// 只支持 {} 初始化时,"=" 才可以省略
int num2{ 5 };
two get6{ 3, 1, 2 };
const two& get7{ 2, 1, 3 };
// 没有 {} 初始化这样会报错
//int num2 5;
//two get8 3, 1, 2;
vector<two> v;
// 容器元素添加时,{} 比有名对象与匿名对象更好
v.push_back(get6); // 有名对象
v.push_back(two(1, 2, 3)); // 匿名对象
v.push_back({ 1, 2, 3 }); // {}
return 0;
}
C++11 中的 std::initializer_list
上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个 vector 对象,如果想用 N 个值去构造初始化,那么我们得实现很多个构造函数才能支持,C++11 库中提出了一个 std::initializer_list 的类, auto il = { 10, 20, 30 },这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list 内部有两个指针分别指向数组的开始和结束。
容器支持一个 std::initializer_list 的构造函数,也就支持任意多个值构成的 { x1, x2, x3… } 进行初始化。STL 中的容器支持任意多个值构成的 { x1, x2, x3… } 进行初始化,就是通过 std::initializer_list 的构造函数支持的。
关于 std::initializer_list 详细讲解可参考这篇文章:(学习总结15)C++11小语法与拷贝问题
C++11 {} 列表初始化 与 std::initializer_list 区别
{} 列表初始化 与 std::initializer_list 同样是使用 {}, 那两者有什么区别呢?答案是 {} 括起来的内容与被初始化的对象:
-
对于 {} 列表初始化,{} 括起来的变量类型可能相同也可能不同,要求是初始化单个对象的。
-
对于 std::initializer_list,{} 括起来的变量类型一定相同,要求是初始化容器的。
#include <iostream>
#include <vector>
using namespace std;
class one
{
int _a;
int _b;
public:
one(int a, int b)
:_a(a)
,_b(b)
{
;
}
};
class two
{
string _name;
int _age;
public:
two(const string& name, int age)
:_name(name)
,_age(age)
{
;
}
};
int main()
{
// {} 列表初始化 初始化单个对象,{} 内元素类型是否相同要根据其构造函数来确定
one get1 = { 1, 2 };
two get2 = { "zhangsan", 20 };
// std::initializer_list 初始化容器,{} 内元素类型相同都为 int
vector<int> arr1 = { 1, 2, 3, 4, 5 };
// 两者结合,
// 外面 {} 是 std::initializer_list 用于初始化容器,元素类型都为 two 或 one,
// 里面 {} 是 列表初始化 用于初始化单个对象
vector<two> arr2 = { { "zhangsan", 20 }, { "lisi", 21 }, { "wanger", 22 } };
vector<one> arr3 = { { 1, 2 }, { 2, 3 }, { 3, 4 }, { 4, 5 }, { 5, 6 }, { 6, 7 } };
// 二维 vector 中,
// 最外层 {} 是 std::initializer_list 用于初始化外层 vector 容器,元素类型都为 vector<two>,
// 第二层 {} 是 std::initializer_list 用于初始化内层 vector 容器,元素类型都为 two,
// 第三层 {} 是 列表初始化 用于初始化单个对象
vector<vector<two>> arr4 = { { { "zhangsan", 20 }, { "lisi", 21 } }, { { "wanger", 22 }, { "mazi", 23 } } };
return 0;
}
二、右值引用
C++98 的 C++ 语法中就有引用的语法,而 C++11 中新增了右值引用语法特性,C++11 之前学习的引用叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
左值和右值
左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时 const 修饰符后的左值,不能给它赋值,但是可以取它的地址。
右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
值得一提的是,左值的英文简写为 lvalue,右值的英文简写为 rvalue。传统认为它们分别是left value、right value 的缩写。现代 C++ 中,lvalue 被解释为 loacte value 的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,而 rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字面量常量,存储于寄存器中的变量等,也就是说左值和右值的核心区别在于能否取地址。
#include <iostream>
using namespace std;
int main()
{
// 左值可以取地址
// 以下是一些常见的左值
int* p = new int(10);
int a = 5;
const int b = 20;
string c = "haha";
cout << &a << endl;
cout << &b << endl;
cout << (void*)&c[0] << endl;
cout << &p << endl;
// 右值不能取地址
// 以下是一些常见的右值
10;
a + b;
fmin(5.1, 6.5);
string("hehe");
//cout << &10 << endl;
//cout << &(a + b) << endl;
//cout << &fmin(5.1, 6.5) << endl;
//cout << &string("hehe") << endl;
return 0;
}
左值引用和右值引用
-
左值引用就是给左值取别名,同样的道理,右值引用就是给右值取别名;
-
左值引用不能直接引用右值,但是 const 左值引用可以引用右值;
-
右值引用不能直接引用左值,但是右值引用可以引用 move(左值)。
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int& r1 = a; // 左值引用
//int& r2 = 10; // 左值引用不能直接引用右值
const int& r3 = 10; // 但是 const 左值引用可以引用右值
int&& rr1 = 10; // 右值引用
//int&& rr2 = a; // 右值引用不能直接引用左值
int&& rr3 = move(a); // 但是右值引用可以引用 move(左值)
return 0;
}
move 是库里面的一个函数模板,本质是进行强制类型转换,它还涉及一些引用折叠的知识。
需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量其变量表达式的属性是左值。
语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看代码中汇编层实现,底层都是用指针实现的,没什么区别。
#include <iostream>
using namespace std;
int main()
{
// 左值可以取地址
// 以下是一些常见的左值
int* p = new int(10);
int a = 5;
const int b = 20;
string c = "haha";
int& r1 = a;
const int& r2 = b;
string& r3 = c;
int*& r4 = p;
// 右值不能取地址
// 以下是一些常见的右值
10;
a + b;
fmin(5.1, 6.5);
string("hehe");
int&& rr1 = 10;
int&& rr2 = a + b;
double&& rr3 = fmin(5.1, 6.5);
string&& rr4 = string("hehe");
string&& rr5 = (string&&)c;
// 右值引用变量其变量表达式的属性是左值,则可以取地址
cout << &rr1 << endl;
cout << &rr2 << endl;
cout << &rr3 << endl;
cout << &rr4 << endl;
// 这里要注意的是,右值引用变量 rr1 的属性是左值,
// 所以不能再被右值引用绑定,除非 move 一下
int& r5 = r1;
//int&& rr6 = rr1;
int&& rr6 = move(rr1);
return 0;
}
引用延长生命周期
右值引用可用于为临时对象延长生命周期。const 的左值引用也能延长临时对象生命周期,但具有 const 属性的对象是无法被修改的。
#include <iostream>
using namespace std;
int main()
{
string str1 = "hello";
string str2 = "world";
//string& r1 = str1 + " " + str2; // 左值引用不能引用右值
const string& r2 = str1 + " " + str2; // const 的左值引用延长临时对象生命周期
//r2 += "!"; // 但对象无法被修改
string&& rr1 = str1 + " " + str2; // 右值引用延长临时对象生命周期
rr1 += "!"; // 对象可以被修改
const string&& rr2 = str1 + " " + str2; // const 的右值引用延长临时对象生命周期
//rr2 += "!"; // 但对象无法被修改
cout << r2 << endl;
cout << rr1 << endl;
cout << rr2 << endl;
return 0;
}
左值和右值的参数匹配
C++98 中,实现一个 const 左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
C++11 以后,分别重载左值引用、const 左值引用、右值引用作为形参的 func 函数,那么实参是 左值 会匹配 func(左值引用),实参是 const 左值 会匹配 func(const 左值引用),实参是 右值 会匹配 func(右值引用)。
右值引用变量在用于表达式时属性是左值,主要是为了能够转移右值里的资源(使用 const 左值引用时引用右值是不能修改右值的),实现移动语义,下面我们讲到移动语义的移动构造与移动赋值场景时,就能体会这样设计的价值。
#include <iostream>
using namespace std;
void func(int& num)
{
cout << "调用左值引用函数 func(" << num << ")" << endl;
}
void func(const int& num)
{
cout << "调用 const 左值引用函数 func(" << num << ")" << endl;
}
void func(int&& num)
{
cout << "调用右值引用函数 func(" << num << ")" << endl;
}
// 当没有 右值引用参数的func,
// 但有 const 右值引用参数的func,
// 会自动匹配 const 右值引用func
void func(const int&& num)
{
cout << "调用 const 右值引用函数 func(" << num << ")" << endl;
}
int main()
{
int a = 10;
const int b = 20;
func(a);
func(b);
func(30);
func(move(a));
func(move(b)); // 注意 move 强制转化时不会将变量的 const 属性丢失
cout << "-------------------" << endl;
// 右值引用变量的属性是左值
int&& rr1 = 40;
func(rr1);
func(move(rr1));
return 0;
}
类型分类
C++11 以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称 prvalue)和将亡值 (expiring value,简称 xvalue)。
纯右值是指那些 字面值常量 或 求值结果相当于字面值 或是 一个无名的临时对象。纯右值和将亡值在 C++11 中提出,C++11 中的纯右值概念划分等价于 C++98 中的右值。
将亡值是指 返回右值引用的函数的调用表达式 和 转换为右值引用的转换函数的调用表达,如 move(左值)、static_cast<X&&>(x)
泛左值(generalized value,简称 glvalue),泛左值包含将亡值和左值。
值类别 - cppreference.com 和 Value categories 这两个关于值类型的中文和英文的官方文档,有兴趣可以了解细节。
变量有名字,就是 glvalue;变量有名字,且不能被 move,就是 lvalue;变量有名字,且可以被 move,就是 xvalu;变量没有名字,且可以被移动,则是 prvalue。
三、移动语义
在 C++11 之前,对象的赋值和传递通常是通过复制来完成的。但是在很多情况下,这种复制操作是不必要的,特别是当对象内部包含一些资源,复制这些资源会带来性能开销。
移动语义允许将资源从一个对象转移到另一个对象,而不是进行代价更大的复制操作。这就好比你要搬家,以前是把所有东西重新买新的(复制)一份放到新家,现在是直接把东西从旧家搬(转移)到新家,这样可以减少浪费,大大提高效率。
移动构造和移动赋值
-
移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
-
移动赋值是一个赋值运算符的重载,它跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
#include <iostream>
using namespace std;
class a
{
private:
int* _p = nullptr;
public:
a(int num = 0)
:_p(new int(num))
{
;
}
~a()
{
if (_p != nullptr)
{
delete _p;
_p = nullptr;
}
}
a(const a& one)
{
cout << "调用拷贝构造 a(const a& one)" << endl;
_p = new int(*(one._p));
}
a& operator=(const a& one)
{
cout << "调用拷贝赋值 a& operator=(const a& one)" << endl;
if (&one != this)
{
_p = new int(*(one._p));
}
return *this;
}
void swap(a& two)
{
std::swap(_p, two._p);
}
a(a&& one) // 我实现的是交换的方式,只要符合要求即可
{
cout << "调用移动构造 a(a&& one)" << endl;
swap(one); // 将资源进行交换
}
a& operator=(a&& one)
{
cout << "调用移动赋值 a& operator=(a&& one)" << endl;
swap(one); // 将资源进行交换
return *this;
}
};
int main()
{
a t1 = 1; // 有参构造
a t2 = t1; // 拷贝构造,拷贝出新资源
a t3 = move(t2); // 移动构造,两者资源交换
t2 = t1; // 拷贝赋值,拷贝出新资源
t3 = move(t2); // 移动赋值,两者资源交换
a t4 = a(5); // 有参构造 + 移动构造,这里 VS2022 优化成只剩 有参构造
return 0;
}
对于像 string / vector 这样的深拷贝的类或者包含深拷贝的成员变量的类,实现移动构造和移动赋值才有意义。因为移动构造和移动赋值的第一个参数都是右值引用的类型,其本质是要判断是否是右值并移动引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从而提高效率。
右值引用与移动语义的关系
右值是一个表达式,它要么是一个临时对象(如函数返回值),要么是一个即将销毁的对象(例如通过 std::move 转换后的对象)。
在没有移动语义的情况下,临时对象或即将销毁的对象 会被复制到调用函数的地方。但有了移动语义和右值引用,调用移动构造函数来直接将资源从它们内部转移到目标对象,而不是重新复制一份资源。
函数重载时参数的右值引用可以匹配临时对象和即将销毁的对象,则可以这样联系两者关系:
-
对于临时对象,右值引用的匹配是自动的,在有移动语义的情况下(有移动构造与移动赋值)会自动将资源进行转移,无形中减少许多不必要的拷贝。
-
对于即将销毁的对象,右值引用的匹配是手动的(例如需要程序员自己用 std::move 标明),在有移动语义且标明的情况下会将资源进行转移,明确的减少拷贝。
-
则右值引用负责匹配,移动语义负责减少其拷贝。
右值引用和移动语义解决传值返回问题
下面是一个简单的大数加法运算,理论上我们可以推测出资源转移的次数,但是考虑到使用的是 VS2022,其进行了很多优化,可能不会与理论上的相同:
#include <iostream>
#include <string>
using namespace std;
string add(const string& one, const string& two)
{
int len1 = one.size() - 1;
int len2 = two.size() - 1;
int next = 0; // 高位存储
string copy;
copy.reserve((len1 > len2 ? len1 : len2) + 5);
while (len1 >= 0 || len2 >= 0 || next > 0)
{
int num1 = len1 >= 0 ? one[len1--] - '0' : 0;
int num2 = len2 >= 0 ? two[len2--] - '0' : 0;
int ret = num1 + num2 + next; // 当前位数计算
next = ret / 10; // 进高位
ret = ret % 10; // 保留低位
copy += (ret + '0'); // 存进结果
}
reverse(copy.begin(), copy.end()); // 翻转
return move(copy); // 第一次转移资源,将 copy 里的资源转移给临时变量
}
int main()
{
string ret = add("1234", "5678"); // 第二次转移资源,将临时变量的资源转移给 ret
cout << ret << endl; // 可以推测 add 里的局部变量 copy 的资源是进行 两次转移 而不是 两次拷贝,提高了效率
return 0;
}
右值引用和移动语义在传参中的提效
查看 STL 文档我们发现 C++11 以后容器的 push 和 insert 系列的接口增加的右值引用版本
- 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间里;
- 当实参是一个右值时,容器内部则调用移动构造,右值对象的资源移动到容器空间的对象中。
这里以 vector 的 push_back 举例:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main()
{
vector<string> arr;
string str1 = "1111111111111111";
arr.push_back(str1); // 拷贝构造
arr.push_back(string("2222222222222222")); // 有参构造 + 移动构造
arr.push_back("3333333333333333"); // 有参构造 + 移动构造
arr.push_back(move(str1)); // 移动构造
for (auto& e : arr)
{
cout << e << endl;
}
return 0;
}
四、引用折叠
C++ 中不能直接定义引用的引用如 int& && r = i; ,这样写会直接报错,通过模板或 typedef 中的类型操作可以构成引用的引用,这时 C++11 给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
#include <iostream>
using namespace std;
typedef int& lref; // 左值引用
typedef int&& rref; // 右值引用
int main()
{
int num = 0;
lref& r1 = num; // 左值引用 + 左值引用 -> 左值引用
lref&& r2 = num; // 左值引用 + 右值引用 -> 左值引用
rref& r3 = num; // 右值引用 + 左值引用 -> 左值引用
rref&& rr1 = 10; // 右值引用 + 右值引用 -> 右值引用
return 0;
}
万能引用
像 func2 这样的函数模板中,T&& x 参数看起来是右值引用参数,但是由于引用折叠的规则,它传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用:
#include <iostream>
using namespace std;
// 由于引用折叠限定,func1 实例化以后总是一个左值引用
template<class T>
void func1(T& x)
{
;
}
// 由于引用折叠限定,func2 实例化后可以是左值引用,也可以是右值引用
template<class T>
void func2(T&& x)
{
;
}
int main()
{
int num = 0;
// 自动类型推导
// func1 可以推导左值,不能推导右值
func1(num);
//func1(0); // 报错
// func2 不仅可以推导左值,也可以推导右值
func2(num);
func2(0);
// 这里显示实例化是方便大家探究
// func1
// 没有折叠->实例化为 void func1(int& x);
func1<int>(num);
//func1<int>(0); // 报错
// 折叠->实例化为 void func1(int& x);
func1<int&>(num);
//func1<int&>(0); // 报错
// 折叠->实例化为 void func1(int& x);
func1<int&&>(num);
//func1<int&&>(0); // 报错
// 折叠->实例化为 void func1(const int& x);
func1<const int&>(num);
func1<const int&>(0);
// 折叠->实例化为 void func1(const int& x);
func1<const int&&>(num);
func1<const int&&>(0);
// func2
// 没有折叠->实例化为 void func2(int&& x);
//func2<int>(num); // 报错
func2<int>(0);
// 折叠->实例化为 void func2(int& x);
func2<int&>(num);
//func2<int&>(0); // 报错
// 折叠->实例化为 void func2(int&& x);
//func2<int&&>(num); // 报错
func2<int&&>(0);
// 折叠->实例化为 void func2(const int& x);
func2<const int&>(num);
func2<const int&>(0);
// 折叠->实例化为 void func2(const int&& x);
//func2<const int&&>(num); // 报错
func2<const int&&>(0);
return 0;
}
万能引用推导细节
Function(T&& x) 函数模板程序中,假设实参是 int 右值,则模板参数 T 推导结果为 int,实现了实参可以是右值。实参是 int 左值,模板参数 T 的推导结果为 int&,再结合引用折叠规则,就实现了实参可以是左值。
则从整体上看,实参是左值,实例化出左值引用版本形参的 Function,实参是右值,实例化出右值引用版本形参的 Function,但是其中还有很多细节:
#include <iostream>
using namespace std;
template<class T>
void Function(T&& x)
{
int one = x;
T derivation = one;
//derivation++;
cout << &one << endl;
cout << &derivation << endl;
}
int main()
{
// 注意这里万能引用自动推导右值时会将 T 推导为 int 而不是 int&&,
// 前者会直接与 && 匹配,而后者会触发引用折叠
// 结果模版实例化为 void Function(int&& x);
// 由于 T 推导为 int,derivation 则是一个 int类型变量
// 所以 one 与 derivation 的地址不一样
Function(0);
// T 推导为 int&,引用折叠,模版实例化为 void Function(int& x);
// derivation 类型为 int&,则它与 one 的地址一样
int a = 10;
Function(a);
// T 推导为 int,模版实例化为 void Function(int&& x);
// derivation 类型为 int,则它与 one 的地址不一样
Function(move(a));
// T 推导为 const int&,引用折叠,模版实例化为 void Function(const int& x);
// derivation 类型为 const int&,它与 one 地址一样,但是不能 ++ 修改
const int b = 20;
Function(b);
// T 推导为 const int,模版实例化为 void Function(const int&& x);
// derivation 类型为 const int,它与 one 地址不一样,不能 ++ 修改
Function(move(b));
return 0;
}
五、完美转发
Function(T&& x)函数模板程序中,传左值实例化以后是左值引用的 Function 函数,传右值实例化以后是右值引用的 Function 函数。
但是结合我们在前面的讲解,左值引用变量 与 右值引用变量 表达式都是左值属性,这意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,则 Function 函数中 x 的属性是左值,那么我们把 x 传递给下一层函数 func1,匹配的都是左值引用版本的 func1 函数。如果想要保持 x 对象的属性,就需要使用完美转发实现。
完美转发 forward 本质是一个函数模板,它主要还是通过引用折叠的方式实现,下面示例中传递给 Function 的实参是右值,T 被推导为 int,没有折叠,forward 内部 x 被强转为右值引用返回;传递给 Function 的实参是左值,T 被推导为 int&,引用折叠为左值引用,forward 内部 x 被强转为左值引用返回。
#include <iostream>
using namespace std;
void func1(int& x)
{
cout << "调用 左值引用 void func1(int& x)" << endl;
}
void func1(const int& x)
{
cout << "调用 const 左值引用 void func1(const int& x)" << endl;
}
void func1(int&& x)
{
cout << "调用 右值引用 void func1(int&& x)" << endl;
}
void func1(const int&& x)
{
cout << "调用 const 右值引用 void func1(const int&& x)" << endl;
}
template<class T>
void Function(T&& x)
{
// 我们知道,右值引用变量自身属性也是左值
// 没有完美转发,func1 这里的 x 一直是一个左值
//func1(x);
// 这里的 T 可以不加上 &&,
// 在之前的程序中已经知道 T 只有两种推导:int 或 int&
// 而 int 在 forward 也为 右值, int& 为左值
func1(forward<T>(x));
//func1(forward<T&&>(x));
}
int main()
{
// 有完美转发会调用 右值的 func1,
// 没有就调用 左值的 func1。
Function(0);
// 调用 左值的 func1
int a = 10;
Function(a);
// 有完美转发调用 右值的 func1,
// 没有调用 左值的 func1
Function(move(a));
// 调用 const 左值的 func1
const int b = 20;
Function(b);
// 有完美转发调用 const 右值的 func1
// 没有调用 const 左值的 func1
Function(move(b));
return 0;
}