C++知识点梳理:移动语意、右值

  1. 右值、rvalue
    1)左值 lvalue,右值 rvalue
    2)左值引用&(int &x = y),右值引用 int &&x。(注意区分右值和右值引用的概念)
    3)const引用可以绑定右值,例如:void func(int const &r);可以传入右值。
    4)使用右值的目的:
    (1)对象是临时的,仅存在函数域内,可以任意操作。
    (2)函数要接管对象的所有权。例如:移动构造函数和移动赋值操作符。
    帮助实现移动语意。
    (3)完美转发
    5)右值时临时值,不存在对应的变量名(否则就变成左值),只用通过“右值引用”引用右值(临时值)。

  2. &&的3种用法:
    1)右值引用:用在具体类型后面,变量名前面。
    2)转发引用(万能引用):用在函数参数列表,且模板类型参数(T或auto)后面,forwarding reference(正式名称,也叫universal reference、万能引用)。
    (1)目的是为了支持perfect forwardding,依赖关系:
    完美转发 perfect forwarding ----> 转发引用 forwarding reference ----> 引用折叠 reference collapsing
    (2)在函数模板的函数参数列表中,声明的类型是T&&,传入的参数类型可能是T, T&, T&&,会有多个&,&&叠加的情况,使用引用折叠规则进行合并。
    (3)不管出入什么类型,T,T&,T&&,折叠后的结果只能是引用,即:T&或者T&&之一。
    (4)配合std::forward< >()使用。std::forward()对lvalue,类似调用std::move,对rvalue右值,do nothing。
    3)函数重载:用在类方法定义后面,可以用于右值对象的专门重载(当类对象是右值时,将会调用带有&&标记的方法)。

// rvalue
string s {
    
    "hello"};
string&& rval = std::move(s);
void func(string&& rs) {
    
    }

// universal reference
template<typename T>
void func(T&& t) {
    
     ... }
void func(auto&& t) {
    
     }

// overload member function for rvalue objects
struct Temp {
    
    
	void func() && {
    
    }
}

auto temp = Temp{
    
    };
temp.func();				// error
std::move(temp).func();		// OK
Temp{
    
    }.func();				// OK
  1. 函数参数:传值、const引用、右值引用

    1)使用传值的方式
    (1)使用const ref作为参数的重载
    a)入参时不会复制参数
    b)在内部要修改参数,需要复制一次。
    一个局部变量,声明和初始化一步完成。
    如果是类成员变量,已经初始化了??????
    c)返回参数时NRVO,不用复制。

    (2)使用rvalue ref作为参数的重载
    a)入参移动,没有复制。
    b)返回值,NRVO优化,不会复制。

    (3)如果使用传值方式实现,要点:
    a)调用时,传入左值或者引用,内部取消显式的复制操作(auto clone = s),在入参传值时自动复制一次。
    把显式的复制操作,转变为复制copy函数自动完成
    b)调用时,传入右值,自动调用移动move构造函数,移动数据,不会复制。

    2)不适合传值的方式作为参数
    (1)Widget.data_ 在构造时已经初始化一次(分配内存)。

    (2)当调用它set_date()方法,
    a)传入lvalue调用,过程中分配内存2次:v->x复制一次(第一次分配内存),x move to data_由于data_内存不够大(data_默认构造的,除非v为空)需要在移动构造函数内再次分配内存(第二次分配内存)。
    过程中涉及2次构造(分配内存),第一次是copy构造(自动调用),第二次移动构造(分内存,且这个步骤不能合并)。所以第一步自动调用copy构造函数没有意义,不如用const ref,节省一次复制操作。

     b)传入右值调用,过程中分配内存一次:v->x移动,不复制;x->data_ 移动构造,分配内存一次。
    

    (3)此时应提供const ref和rvalue重载方法
    a)如果传入lvalue,调用const ref重载方法,省去一次复制操作。const ref版本对传值操作有优化。
    b)如果传入rvalue,调用rvalue重载方法。过程和使用传值方式一样(自动调用移动构造),只是这里显示作为rvalue传入,过程中分配内存一次。

    结论:
    1)只有声明和初始化一次完成局部变量(临时或函数内部),如果传入const ref,这里会进行一次复制。使用copy构造自动完成这个复制,或者说替代显式复制。
    关键:初始化和复制一步完成
    本质上,就是这个局部变量是多余的,通过传值在函数参数初始化时创建一个临时变量(省略掉多余的局部变量),函数参数的变量名是必须有的,只是能够选择分配内存(传值、赋值)或不分配内存(传引用、右值)。
    2)如果赋值对象为类成员变量(构造时已经初始化过),赋值时是再次复制。
    关键:初始化和复制是分开进行的,这种不能用复制构造进行简化,也就不能用传值方式声明入参。
    3)只要有移动构造函数,使用rvalue重载还是传值,对过程没有影响,没有额外的复制(分配内存)。

// Argument s is a const reference
auto str_to_lower(const std::string& s) -> std::string {
    
    
	auto clone = s;
	for (auto& c: clone) c = std::tolower(c);
	return clone;
}
// Argument s is an rvalue reference
auto str_to_lower(std::string&& s) -> std::string {
    
    
	for (auto& c: s) c = std::tolower(c);
	return s;
}

// 用传值的方式,整合上述 2中情况
auto str_to_lower(std::string s) -> std::string {
    
    
	for (auto& c: s) c = std::tolower(c);
	return s;
}
// 调用
auto str = std::string{
    
    "ABC"};
str = str_to_lower(str);
str = str_to_lower(std::move(str));


// 不适合用传值方式传递参数
class Widget {
    
    
	std::vector<int> data_{
    
    };
	// ...
public:
	void set_data(std::vector<int> x) {
    
    
		data_ = std::move(x);
	}
};

auto v = std::vector<int>{
    
    1, 2, 3, 4};
widget.set_data(v);

// const ref 重载
void set_data(const std::vector<int>& x) {
    
    
	data_ = x;
}
// rvalue 重载
void set_data(std::vector<int>&& x) noexcept {
    
    
	data_ = std::move(x);
}

  1. 完美转发 perfect forwarding

    要实现完美转发,需要3个方面的保证:
    1)入参使用:forwarding reference
    2)函数内部使用:std::forward< T >()
    3)返回类型使用:decltype( auto )

    其中1/2是必须的,入参并转给调用的函数;
    第3条是可选的,取决于是否需要把调用函数的返回值“完美转发”。

猜你喜欢

转载自blog.csdn.net/yinminsumeng/article/details/134380691