c++ copy elision

在函数结束,返回一个对象时,照理来说,会调用其拷贝构造函数,但这无疑是一种浪费。在语言的发展过程中,对此有很多优化,这些优化有的来自编译器的实现,有的来自标准的规定

下面分阶段介绍一下:

第一阶段:两次拷贝(其实没有存在过)

//test.cpp
#include <iostream>
#include <vector>

using namespace std;

class Test {
  public:
    Test() {
      cout << "Test Constructor..." << endl;
    }
    Test(const Test &other) {
      cout << "Test Copy Constructor..." << endl;
    }

};

Test getTest() {
  Test t;
  return t;
}
int main() {
  Test t = getTest();
  return 0;
}
$ g++ test.cpp -o test
$ ./test 
Test Constructor...

通过执行上面程序可以发现,程序只调用了一次构造函数,并没有调用拷贝构造,这是由于编译器默认已经进行了copy elision(就是不copy了,直接在函数调用的地方构造),所有这种情况其实是下面第二阶段的情况。

要想看到原始的情况,需要手动禁用copy elision:(指定-fno-elide-constructors)

$ g++ test.cpp -o test -fno-elide-constructors
$ ./test 
Test Constructor...
Test Copy Constructor...
Test Copy Constructor...

第二阶段: 编译器自己实现了copy elision(C++17之前)

上面提到了,demo就是上面的不指定-fno-elide-constructors的情况。

第三阶段:标准规定RVO要用copy elision(c++17之后)

先来说明一下什么叫RVO,RVO(return value optimation)是与NRVO(named return value optimizatioin)对应的一个概念,就我们的demo来说:

Test getTest() { //RVO
  return Test();
}
Test getTest() { //NRVO
  Test t;
  return t;
}

也就是说,NRVO返回的对象有自己的名字,而RVO则没有。

所以C++17的规定可以解释为:如果返回对象没有名字,则必须采用copy elision,不能调用其拷贝构造函数。

这里要澄清一个事实,虽然在此之前,很多编译都已经进行了copy elision,但这并不是必须的。因为标准没规定,其实是编译器自作主张,将调用其拷贝构造的过程优化了。

下面的demo可以说明一些问题:

//test.cpp
#include <iostream>
#include <vector>

using namespace std;

class Test {
  public:
    Test() {
      cout << "Test Constructor..." << endl;
    }
    Test(const Test &other) = delte;

};

Test getTest() {
  Test t;
  return t;
}
int main() {
  Test t = getTest();
  return 0;
}
[chenyifei@chenyifei-ThinkPad-L14-Gen-1 move]$ g++ test.cpp -o test
test.cpp: In function ‘Test getTest()’:
test.cpp:18:10: error: use of deleted function ‘Test::Test(const Test&)’
   18 |   return t;

意思是,虽然编译器按照copy elision,不会调用其拷贝构造函数,但类必须得有拷贝构造函数,这就是因为编译器必须要保证语法正确,才能进行copy elision优化。

但当C++标准规定必须要copy elision后,就可以没有拷贝构造函数了:

  1 //test.cpp
  2 #include <iostream>
  3 #include <vector>
  4 
  5 using namespace std;
  6 
  7 class Test {
  8   public:
  9     Test() {
 10       cout << "Test Constructor..." << endl;
 11     }
 12     Test(const Test &other) = delete;
 13 
 14 };
 15 
 16 Test getTest() { //RVO
 17   return Test();
 18 }
 19 int main() {
 20   Test t = getTest();
 21   return 0;
 22 }
$ g++ test.cpp -o test
test.cpp: In function ‘Test getTest()’:
test.cpp:17:15: error: use of deleted function ‘Test::Test(const Test&)’
   17 |   return Test();
      |               ^
test.cpp:12:5: note: declared here
   12 |     Test(const Test &other) = delete;
      |     ^~~~
test.cpp: In function ‘int main()’:
test.cpp:20:20: error: use of deleted function ‘Test::Test(const Test&)’
   20 |   Test t = getTest();
      |                    ^
test.cpp:12:5: note: declared here
   12 |     Test(const Test &other) = delete;
$ g++ test.cpp -o test -std=c++17
$ ./test 
Test Constructor...

注意:仅限于RVO。

考虑移动构造

在函数返回对象的时候,移动构造的也会涉及其中。因为返回的局部对象(无论是RVO还是NRVO)都会判定为右值,所以(如果需要拷贝的话)优先匹配移动构造。但注意copy elision是在“如果需要拷贝”前面的,也就是说由于编译器无论是RVO还是NRVO,都已经作了优化,所以在实际操作过程中,几乎不会碰到遇到需要拷贝的情况,就算指定了-fno-elide-constructors,也会优先匹配移动构造函数。只有移动构造函数不存,才会匹配拷贝构造函数。

这里还有个小情况:

warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]

意思是如果手动给返回对象加上了std::move()则优先调用移动构造,不再进行copy elision优化。

所以除非你的语义是:必须要调用移动构造函数去做移动之外的事情。否则,给返回值加std::move()是个错误的选择
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值