c++拾遗=右值, move, 转发
参考¶
"从4行代码看右值引用": https://www.cnblogs.com/qicosmos/p/4283455.html
//
// Created by zhangy233 on 2021/1/6.
//
#include <iostream>
using namespace std;
class A{
public:
A(){
cout << "A 默认" << endl;
}
A(A const &a){
cout << "A 拷贝" << endl;
}
~A(){
cout << "A 析构" << endl;
}
};
void stu_01(){
cout << "探究使用匿名变量创建对象和使用对象 的性能差距" << endl;
// A a = A(); // 使用表达式, 创建匿名对象右值, 再绑定到左值; 会先发生一次默认构造函数用来生成 临时变量右值, 然后拷贝构造函数生成一个新的变量绑定到左值 a, 随后临时变量右值销毁析构, 随后程序结束 左值 a 被析构
A a; // 使用类, 这种方法, 只发生了一次默认和析构(很明显的, 这种性能会更高)
}
void stu_02(){
cout << "探索表达式中使用左值绑定临时变量的变化" << endl;
auto get_A = []()->A{ // 'b
return A();
};
//'a
A a = get_A();
// 如下结果
// A 默认 函数体内 return A() 生成的一个对象生命周期为 'b,
// A 拷贝 函数栈return出函数内生成的一个对象, 这是栈内的对象copy到栈外生成一个临时变量, 该临时变量的生命周期只存在当前行
// A 析构 函数栈结束, 析构栈内资源, 'b 生命周期结束
// A 拷贝 get_A() 函数的返回值也是一个表达式, get_A()也有其生命周期, 临时变量发生一次copy, 为变量a 赋值, a的生命周期为 'a
// A 析构 表达式 A a = get_A() 结束, 临时变量发生析构
// A 析构 函数执行结束, 'a 生命周期结束, 对栈内资源进行析构销毁, 销毁了变量 a
//
/* 伪代码
stu_02{ 'a
A a = { 'b
get_A(){ 'c
return A()
} ~'c
} ~'b
} ~'a
*/
}
void stu_03(){
cout << "探索表达式使用右值引用来绑定临时变量的变化" << endl;
auto get_A = []()->A{ // 'b
return A();
};
A&& a = get_A();
// A& b = get_A(); // 错误, 非常量左值引用只能接受左值
const A& c = get_A(); // 在 c++98 中, 常量左值引用是一个“万能”的引用类型,可以接受左值、右值、常量左值和常量右值
// 结果如下, 通过右值引用,比之前少了一次拷贝构造和一次析构,原因在于右值引用绑定了右值, 而不是用左值进行绑定
// 其 A&& a = get_A() 表达式产生的临时变量(右值), 其生命周期被延长到了和变量 a 一样长, a直接拿到了 临时变量的引用, 而不用再把临时变量拷贝到a上
// A 默认
// A 拷贝
// A 析构
// A 析构
}
template<typename T>
void f(T&& t){}
void processValue(int& a){ cout << "lvalue" << endl; }
void processValue(int&& a){ cout << "rvalue" << endl; }
template <typename T>
void forwardValue(T&& val)
{
processValue(std::forward<T>(val)); //照参数本来的类型进行转发, 调用结果是 lvalue 和 rvalue
}
template <typename T>
void no_forwordValue(T&& val){
processValue(val); // 这里函数的参数再函数内, 由于参数有一个赋值操作, 参数val变成了一个左值
}
int main(){
cout << "编译命令(关闭编译器优化): g++ rvalue.cpp -o test -std=c++11 -fno-elide-constructors" << endl;
cout << "鸣谢: https://www.cnblogs.com/qicosmos/p/4283455.html" << endl;
// 所有的具名变量或对象都是左值,而匿名变量则是右值,比如,简单的赋值语句 int a = 0, i 是左值,0 是字面量,就是右值。在上面的代码中,i 可以被引用,0 就不可以了
// 在C++11中所有的值必属于左值("左值"是表达式的结果的一种属性)、将亡值、纯右值三者之一
// 区分左值和右值的一个简单办法是:看能不能对表达式取地址,如果能,则为左值,否则为右值
// 探索使用 表达式匿名对象创建变量 和 直接使用类创建变量 的性能差距
// stu_01();
// 探索表达式中使用左值绑定临时变量的变化
// stu_02();
// 探索表达式使用右值引用来绑定临时变量的变化
// stu_03();
// 1: 右值引用独立于左值和右值。意思是右值引用类型的变量可能是左值也可能是右值, 如下
// var1类型为右值引用,但var1本身是左值,因为具名变量都是左值
int&& var1 = 123;
// 2: T&&表示的值类型不确定,可能是左值又可能是右值
// void f(T&& t){} 函数的签名, 可以接受左值, 也可以接受右值引用
// T&& t在发生自动类型推断的时候,它是未定的引用类型(universal references),如果被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化
// 对于函数template<typename T>void f(T&& t),当参数为右值10的时候,根据universal references的特点,t被一个右值初始化,那么t就是右值;当参数为左值x时,t被一个左值引用初始化,那么t就是一个左值
// 需要注意的是,仅仅是当发生自动类型推导(如函数模板的类型自动推导,或auto关键字)的时候,T&&才是universal references
int a1 = 213;
f(a1); // 传入左值
f(20); // 传入右值
// 3: move实际上它并不能移动任何东西,它唯一的功能是将一个左值强制转换为一个右值引用
// C++11中所有的容器都实现了移动语义,方便我们做性能优化
std::initializer_list< std::string> tokens1;
std::initializer_list< std::string> t1 = tokens1; //这里存在拷贝
std::initializer_list< std::string> tokens2;
std::initializer_list< std::string> t2 = std::move(tokens2); //这里没有拷贝, 只是转移其所有权
// 4. 完美转发: 在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),将参数传递给函数模板中调用的另外一个函数
// T&& 可以使左值也可以是右值, 在传参时, 我们希望传入的参数再函数内保留参数的特征
// forwardValue 实现了完美转发, no_forwordValue则没有使用转发导致变量传入到函数内由于函数的参数本有一个赋值表达式存在, 函数的参数都变成了左值
// 右值引用T&&是一个universal references,可以接受左值或者右值,正是这个特性让他适合作为一个参数的路由,然后再通过std::forward按照参数的实际类型去匹配对应的重载函数,最终实现完美转发
int i = 0;
cout << "forwordValue: " << endl;
forwardValue(i); // 传入左值
forwardValue(0);// 传入右值
cout << "no_forwordValue: " << endl;
no_forwordValue(i);
no_forwordValue(0);
return 0;
}