C++ copy elision

C++ copy elision

环境

gcc version 9.4.0
c++14

示例代码

//test.cpp
#include <iostream>
class Test {
  public:
    Test() {
      std::cout << "Test Constructor..." << std::endl;
    }
    Test(const Test &other) {
      std::cout << "Test Copy Constructor..." << std::endl;
    }
};

Test getTest() {
  Test t;
  return std::move(t);
}
int main() {
  Test t = getTest();
  return 0;
}

编译错误

编译选项为 -Werror

  • 编译指令
g++ test.cpp -o test -Werror
  • 报错如下:
test.cpp:766:19: error: moving a local object in a return statement prevents copy elision [-Werror=pessimizing-move]
  766 |   return std::move(t);
      |          ~~~~~~~~~^~~
test.cpp:766:19: note: remove ‘std::move’ call
cc1plus: all warnings being treated as errors

不指定 -Werror 编译选项

  • 编译指令
g++ test.cpp -o test
  • 报错如下:
xx.cpp:761:19: warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
  761 |   return std::move(t);
      |          ~~~~~~~~~^~~
xx.cpp:761:19: note: remove ‘std::move’ call

报错解释

错误(警告):返回语句中使用std::move移动一个本地对象,会阻止copy elision优化。
提示:移除 ‘std::move’ 调用

解决办法

就如编译器提示的,在函数中返回一个本地对象时,直接返回即可,不要加上std::move。

深入理解copy elision

RVO 和NRVO

在C++17之前,标准并没有规定copy elision,编译器帮我们实现copy elision
在C++17及之后,标准从编译器借取经验,实现了copy elision。并规定RVO(return value optimization)必须要用copy elision,而NRVO(named return value optimization)则可以不用(由编译器决定)。
其中RVO指返回无名字的对象,NRVO正好相反表示返回有名字的对象。

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

手动禁用编译器的copy elision

编译时,通过编译选项-fno-elide-constructors告诉编译器不要帮我们 copy elision。

测试:C++14中copy elision

NROV

  • 代码
Test getTest() { // NRVO
  Test t;
  return t;
}
编译器参与copy elision
  • 编译
g++ test.cpp -o test -std=c++14
  • 运行
Test Constructor...
  • 现象
    按照正常的程序执行流程,getTest函数在返回时执行一次拷贝构造函数,在main函数中将getTest的返回值赋值给 t 时再执行一次拷贝构造函数。
    期待的运行结果应该是调用一次构造函数,两次拷贝构造函数,即:
Test Constructor...
Test Copy Constructor...
Test Copy Constructor...

但通过上面测试我们发现,程序只调用了一次构造函数,并没有调用拷贝构造函数。这是由于编译器默认已经进行了copy elision(就是不copy了,直接在函数调用的地方执行构造)。
这是一种非常高效的编译器优化,帮助我们省了两次拷贝构造操作。

编译器不参与copy elision
  • 编译
g++ test.cpp -o test -std=c++14 -fno-elide-constructors
  • 运行
Test Constructor...
Test Copy Constructor...
Test Copy Constructor...
  • 现象
    正如前面期待的,程序进行了两次拷贝构造函数的调用。

ROV

  • 代码
Test getTest() { // RVO
  return Test();
}
编译器参与copy elision
  • 编译
g++ test.cpp -o test -std=c++14
  • 运行
Test Constructor...
编译器不参与copy elision
  • 编译
g++ test.cpp -o test -std=c++14 -fno-elide-constructors
  • 运行
Test Constructor...
Test Copy Constructor...
Test Copy Constructor...

小结

在C++14中,无论是RVO还是NRVO,copy elision都是由编译器实现的。

测试:C++17中copy elision

NRVO

  • 代码
Test getTest() { // NRVO
  Test t;
  return t;
}
编译器参与copy elision
  • 编译
g++ test.cpp -o test -std=c++17
  • 运行
Test Constructor...
编译器不参与copy elision
g++ test.cpp -o test -std=c++17 -fno-elide-constructors
  • 运行
Test Constructor...
Test Copy Constructor...

RVO

  • 代码
Test getTest() { // RVO
  return Test();
}
编译器参与copy elision
  • 编译
g++ test.cpp -o test -std=c++17
  • 运行
Test Constructor...
编译器不参与copy elision
g++ test.cpp -o test -std=c++17 -fno-elide-constructors
  • 运行
Test Constructor...

小结

在C++17中,标准实现了copy elision,编译器终于可以退居二线。
在返回RVO临时对象时,必须采用copy elision,不能调用其拷贝构造函数,前面测试可以看出无论是否禁用编译器的copy elistion功能,程序都不会调用拷贝构造函数。
但在返回NRVO临时对象时,C++好像有些偷懒,不像编译器会把所有的拷贝构造函数copy elision掉,只copy elision掉了一次。

那么NRVO情况下,究竟copy elision掉的是哪次的拷贝构造函数?下面通过测试来一探究竟:

NRVO copy elision探究

我们去掉最后一次拷贝构造,即在函数返回临时对象时,我们不接收。

  • 代码
Test getTest() {
  Test t;
  return std::move(t);
}
int main() {
  getTest();
  return 0;
}
  • C++17编译
g++ test.cpp -o test -std=c++17 -fno-elide-constructors
  • 运行
Test Constructor...
Test Copy Constructor...
  • 现象
    我们发现运行结果与代码修改前一致。
  • 结论
    C++17 copy elision掉的就是最后一次拷贝构造函数,即将函数返回的临时对象拷贝到主函数临时对象的这一次。
    因此在,NRVO情况下,在将临时对象进行返回前还是会进行一次复制,只在将复制后的对象用来构造主函数的临时对象时才采用copy elision。

既然编译器会帮我们copy elision,那么是不是就可以删除掉拷贝构造函数?

测试NRVO,删除掉拷贝构造函数

  • 代码
//test.cpp
#include <iostream>
class Test {
  public:
    Test() {
      std::cout << "Test Constructor..." << std::endl;
    }
    Test(const Test &other) = delete;
};

Test getTest() {
  Test t;
  return t;
}
int main() {
  Test t = getTest();
  return 0;
}
  • 编译运行 C++17
david@DESKTOP-J901GTO:~$ g++ test.cpp  -o test -std=c++17
test.cpp: In function ‘Test getTest()’:
test.cpp:21:10: error: use of deleted function ‘Test::Test(const Test&)21 |   return t;
      |          ^
test.cpp:10:5: note: declared here
   10 |     Test(const Test &other) = delete;
      |     ^~~~
  • 编译运行 C++14
david@DESKTOP-J901GTO:~$ g++ test.cpp  -o test -std=c++14
test.cpp: In function ‘Test getTest()’:
test.cpp:20:18: error: use of deleted function ‘Test::Test(const Test&)20 |   Test t; return t;
      |                  ^
test.cpp:10:5: note: declared here
   10 |     Test(const Test &other) = delete;
      |     ^~~~
  • 现象
    编译报错,这是NRVO情况下,那么在RVO情况下呢

测试RVO,删除掉拷贝构造函数

将上面代码改为RVO

  • 代码
Test getTest() {
  return Test();
}
  • 编译运行 C++17
david@DESKTOP-J901GTO:~$ g++ test.cpp  -o test -std=c++17
david@DESKTOP-J901GTO:~$ ./test
Test Constructor...
  • 编译运行 C++14
david@DESKTOP-J901GTO:~$ g++ test.cpp  -o test -std=c++14
test.cpp: In function ‘Test getTest()’:
test.cpp:21:15: error: use of deleted function ‘Test::Test(const Test&)21 |   return Test();
      |               ^
test.cpp:10:5: note: declared here
   10 |     Test(const Test &other) = delete;
      |     ^~~~

结论

针对RVO情况,如果在C++17及以上,可以删除拷贝构造函数。
其它所有情况,不能删除拷贝构造函数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值