右值引用与移动语义

右值引用与移动语义

右值引用的概念

C++中所有的表达式和值,要么是左值,要么是右值。
通俗的来说,左值指可以使用&取得其地址的“非临时对象”,而右值则是指不可用&取得其地址的“临时对象”。

类比于左值引用,右值引用也仅可以使用右值赋值。我们用T &&来表示T类型的右值引用

int &&rval1 = 1;    // 正确,1是右值,可以赋值给右值引用
int a = 1;
int &&rval2 = a;    // 错误,a是左值,不可以赋值给右值引用

void f(int &a) {
  std::cout << "Left Version" << std::endl;
}

void f(int &&a) {
  f(a); // 这里a已经成为一个int &&类型的左值,使用左值版本
  std::cout << "Right Version" << std::endl;
}

int main() {
  int a = 0;
  f(a);       // 左值版本
  f(a + 1);   // 注意:表达式(临时对象)也是右值,所以这里是右值版本
}

编译器返回值优化技术,简称RVO,功能是消除为保存函数返回值而创建的临时对象,使用-fno-elide-constructors关闭此优化,然后执行下面的用例观察其行为。

string foo(void) {
  return "hello";
}
void bar(string & s) {
  std::cout << s << std::endl;
}

void sayhi(string && s) {
  std::cout << s << std::endl;
}

sayHi(std::string("hello")); //右值引用
bar(foo()); //编译报错

c++标准规定,编译器自行构造的临时对象一定是const的。foo()返回一个临时对象,而bar需要一个左值引用,其试图将一个 const 类型的对象转换为非 const 类型,这是非法的。const &是可以重新构造临时对象,非const &不可以,所以解决方法是修改bar参数为(const string & s)或(string && s)

结论

引用型参数应该在能被定义为 const 的情况下,尽量定义为const的。

发生拷贝构造的三个时机以及为什么拷贝构造是常引用传参?

  1. 用类的一个对象去初始化另一个对象时
  2. 当函数的形参是类对象值传递,如果是引用传递则不会调用
  3. 当函数的返回值是类对象(非引用)时

示例CopyCtr

//Test为类名
Test& show(Test t) {
  return t;
}

Test t3(show(Test()));
/* Test() 默认构造
 * show() 传参时调用拷贝构造(情况2)
 * show() 返回时调用拷贝构造(情况3)
 * t3() 接收show()返回值调用拷贝构造(情况1) 
 */
  • 如果拷贝构造参数是值传递,那么在建立这个参数的时候也需要调用拷贝构造,会陷入无穷递归。
  • 如果拷贝构造参数是non-const引用传递,对于上述情况3,c++标准规定函数返回的,由编译器自行构造的临时对象一定是const,无法作为拷贝构造(情况1)的参数。

为什么只有赋值/移动赋值需要自身赋值判断,而构造/拷贝构造/移动构造都不需要?

因为只有当对象不存在时才会调用构造,否则只会调用赋值

示例Move

class Test {
 public:
  Test(int a = 0) {//普通构造函数
    d = new int(a);
    cout << "构造函数" << endl;
  }

  Test(const Test &rhs) {//拷贝构造函数
    d = new int;
    *d = *(rhs.d);
    cout << "拷贝构造函数" << endl;
  }

  Test &operator=(Test &rhs) noexcept {//拷贝赋值函数
    if (this == &rhs) return *this;

    d = new int(*(rhs.d));
    cout << "拷贝赋值函数" << endl;
    return *this;
  }
#if 0
  Test(Test &&tmp) noexcept {//移动构造函数
    d = tmp.d;
    tmp.d = nullptr;
    cout << "移动构造函数" << endl;
  }

  Test &operator=(Test &&rhs) noexcept {//移动赋值函数
    if (this == &rhs) return *this;

    d = rhs.d;
    rhs.d = nullptr;
    cout << "移动赋值函数" << endl;
    return *this;
  }
#endif

  ~Test() {//析构函数
    if (d != nullptr) {
      delete d;
      cout << "delete d" << endl;
    }
    cout << "析构函数" << endl;
  }

  int *d;
};


Test GetTmp() {
  Test t;
  return t;
}

在未定义移动语义的情况下

执行Test obj = GetTmp() 输出如下:

构造函数
拷贝构造函数
delete d
析构函数
拷贝构造函数
delete d
析构函数
delete d
析构函数

分析:
GetTmp函数里先构造t,在返回时以t为参数调用拷贝构造来构造临时对象返回值(假设为tmp),之后t销毁,然后在以tmp为参数调用拷贝构造来生成obj,然后tmp销毁,程序结束后obj销毁。

在未定义移动语义的情况下

执行Test&& obj = GetTmp() 输出如下:

构造函数
拷贝构造函数
delete d
析构函数
delete d
析构函数

分析:
GetTmp函数里先构造t,在返回时以t为参数调用拷贝构造来构造临时对象返回值(假设为tmp),之后t销毁,然后定义tmp的右值引用obj(obj本身是个左值,这一步没有调用任何构造),tmp的生命周期被延长,程序结束后obj(即tmp)销毁。

在定义移动语义的情况下

执行Test&& obj = GetTmp() 输出如下:

构造函数
移动构造函数
析构函数
delete d
析构函数

分析:
GetTmp函数里先构造t,在返回时以t为参数调用移动构造函数来构造临时对象返回值(假设为tmp),之后t销毁,然后定义tmp的右值引用obj(obj本身是个左值,这一步没有调用任何构造),tmp的生命周期被延长,程序结束后obj(即tmp)销毁。对比上一种情况,这里析构时只释放了一次资源(d, 说明也只申请了一次资源)。

为什么需要move语义

示例Move显示拷贝构造和赋值操作生成的临时对象,其创建创建、拷贝以及销毁对性能有严重影响。

move语义(通过右值引用支持)可以将资源(堆,系统对象等)从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。

move语义的定义约束

  • 参数(右值)的符号必须是右值引用符号,即“&&”。
  • 参数(右值)不可以是常量,因为我们需要修改右值。
  • 参数(右值)的资源链接和标记必须修改,否则,右值的析构函数就会释放资源,转移到新对象的资源也就无效了。

std::move

std::move是一个模板函数,作用不是真的“移动”对象,是将参数强制转换为右值。这样配合移动构造函数和移动赋值运算符,我们就可以实现移动语义。

真正的“移动”是通过移动赋值运算符和移动构造函数进行的,与std::move并没有关系,他只是在“显式地声明放弃控制权”。

std::string a { "hello, world" };
std::string b = std::move(a);
//just like this
std::string b = static_cast<std::string &&>(a);

注意

  • 若不手动地声明移动构造函数和移动赋值操作符,那么C++将会使用拷贝语义的版本。
  • POD对象不适合使用移动。因为无论如何,POD对象中的成员仍需要被复制。
  • 资源管理对象应当配置移动语义。一般而言这些类都会被声明为“不可复制”的,配置移动语义可以使得操作这个类更加方便。
  • 需要大量的额外资源的对象可以配置移动语义,这样将提供减少不必要赋值的机会。

当且仅当“移动”比“拷贝”更迅速时才应当使用,若两者时间相差不大甚至“拷贝”更迅速时,采用“拷贝”是更好的选择。

参考文章

https://blog.csdn.net/hepangda/article/details/88562995
https://tennysonsky.blog.csdn.net/article/details/77159204

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值