C++返回值拷贝以及std::move

最近在学习《effective modern C++》,到条款二十五,看到了C++返回值拷贝的说明,故做一下总结。并参考了网上的资料深入理解C++中的RVO

概述

我将以下面的类为例子,说明C++在RVO的情况下以及关闭RVO情况下,其函数返回值是如何返回给调用者的,并给出其汇编代码的说明。环境是Ubuntu Linux,编译器为GCC7.5.0。

class Obj {
public:
    Obj() { // 构造函数
    std::cout << "in Obj() " << " " << this << std::endl;
    }
    Obj(int n) {
    std::cout << "in Obj(int) " << " " << this << std::endl;
    }
    Obj(const Obj &obj) { // 拷贝构造函数
    std::cout << "in Obj(const Obj &obj) " << &obj << " " << this << std::endl;
    }
    Obj(const Obj &&obj) { // 移动构造函数
    std::cout << "in Obj(const Obj &&obj) " << &obj << " " << this << std::endl;
    }
    Obj &operator=(const Obj &obj) { // 赋值构造函数
    std::cout << "in operator=(const Obj &obj)" << std::endl;
    return *this;
    }
    Obj &operator=(const Obj &&obj) { // 移动赋值构造函数
    std::cout << "in operator=(const Obj &&obj)" << std::endl;
    return *this;
    }
    ~Obj() { // 析构函数
    std::cout << "in ~Obj() " << this << std::endl;
    }
private:
    int n;
};

关闭RVO

编译命令加上-fno-elide-constructors表示关闭RVO 优化开关。定义函数fun1,并调用。

//test28_RVO.cpp
Obj fun1() {
  Obj obj;
  // do sth;
  return obj;
}

int main() {
    Obj a = fun1();
    return 0;
}

编译:g++ -fno-elide-constructors -g -o test28_RVO test28_RVO.cpp。 结果打印:如下图所示。

  • 定义拷贝构造函数以及移动构造函数的情况下:

  • 定义拷贝构造函数而未定义移动构造函数的情况下:

结果分析:从打印可以分析出,在关闭RVO优化的情况下,C++返回一个对象会调用拷贝构造函数(或者移动构造函数)2次。分析其汇编代码,可知,在main函数调用函数fun1()的时候,调用者会将一个栈的地址传递给fun1()。在函数fun1()内,当调用构造函数创建obj之后,会将obj拷贝到这个栈的地址,作为一个临时对象。函数fun()调用结束之后,会再调用一次拷贝构造函数,临时对象拷贝到接收者即a中。

在RVO下

代码和上面的相同。

//test28_RVO.cpp
Obj fun1() {
  Obj obj;
  // do sth;
  return obj;
}

int main() {
    Obj a = fun1();
    return 0;
}

编译:g++ -g -o test28_RVO test28_RVO.cpp。 结果打印:如下图所示。

结果分析:在启用RVO优化的情况下,只调用了一次构造函数,拷贝构造函数没有被调用。 从汇编代码结果可以看出,main函数将地址-0x1c(%rbp)传递给函数fun1(),之后,fun1()在调用Obj构造函数的时候,直接在这个地址上进行构造,免去了临时对象的创建以及拷贝。

RVO的条件

《modern effective c++》中指出,在开启RVO之后,返回局部对象只有在满足以下两个条件的情况下,编译器才会进行RVO优化。 (1)局部对象与函数返回值的类型相同。 (2)局部对象就是要返回的东西。函数形参不满足要求。 注意:当函数不同控制路径返回不同局部变量时,不会进行拷贝消除的操作,因为其不知道要返回哪个对象。

返回值为std::move的情况

代码如下:

Obj fun2() {
  Obj obj;
  // do sth;
  return std::move(obj);
}

int main() {
    Obj a = fun2();
    return 0;
}

编译:g++ -g -o test28_RVO test28_RVO.cpp。 结果打印:如下图所示

  • 定义拷贝构造函数以及移动构造函数的情况下:

  • 定义拷贝构造函数,未移动构造函数的情况下(之所以编译器未报错,是因为const修饰的左值形参能够接收一个右值实参):

结果分析:从汇编代码中可以看出,main将变量a的地址传递给fun2()fun2()创建Obj obj,然后调用移动构造函数将obj移动到a中。

Tags: c++语法
Share: X (Twitter) Facebook LinkedIn