【转】C++中的返回值优化

C++中的返回值优化

在这边文章里用到了以下编译器和操作系统,大家请自行安装

$ uname -ar
Linux debian 4.9.0-3-amd64 #1 SMP Debian 4.9.30-2+deb9u3 (2017-08-06) x86_64 GNU/Linux

$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Debian GNU/Linux 9.1 (stretch)
Release:        9.1
Codename:       stretch

$ clang++ --version
clang version 5.0.0 (tags/RELEASE_500/final)
Target: x86_64-unknown-linux-gnu
Thread model: posix

$ g++ --version
g++ (Debian 6.3.0-18) 6.3.0 20170516
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

首先我们先来看一道题,下面的代码运行之后会输出什么结果?

#include <iostream>
using namespace std;

class C {
public:
  C() { cout <<  "Constructor" << endl; }
  C(const C&) { cout << "Copy Constructor" << endl; }
  ~C() { cout << "Destructor" << endl; }
};

C foo() {
  C c1;
  return c1;
}

int main() {
    foo();
    return 0;
}
A.
Constructor
Copy Constructor
Destructor
Destructor

B.
Constructor
Destructor

我想大多数人会选A,对吗?因为foo函数在返回C类的对象时会调用拷贝构造函数来创建一个临时对象。

现在让我们编译并运行这个程序,看看输出结果是否如我们所料

$ clang++ -std=c++11 foo.cpp
$ ./a.out 
Constructor
Destructor

然而,遗憾的是,事实与课本里说的并不一样,那么,为什么会这样呢?从实际编译运行的输出来看,C类的拷贝构造函数并没有被调用。
这是因为在实际工程中大多数时候C++构造对象的开销巨大,编译器为了生成高效的代码,在foo函数返回时并没有调用拷贝构造函数去生成一个临时对象,而是直接使用在foo函数内初始化的对象c1作为返回值传出去。这就是C++中的返回值优化(Return Value Optimization, RVO)。

由于较新版本的gcc/clang均已默认开启了返回值优化(即使没有加上优化选项),我们想看到书上所说的现象只能在编译的时候加上特殊选项,让编译器不要做返回值优化,操作步骤如下:

$ clang++ -std=c++11 -fno-elide-constructors foo.cpp
$ ./a.out 
Constructor
Copy Constructor
Destructor
Destructor

返回值优化能在一定程度上提高程序的运行效率,但是我们在实际工程中最好不要依赖这个编译器优化特性,因为它要求的条件非常苛刻(或者说,编译器还不够聪明:P)。让我们在foo函数中加上一个无用的条件判断语句,试试编译器是否能继续应用返回值优化。

#include <iostream>
using namespace std;

class C {
public:
  C() { cout <<  "Constructor" << endl; }
  C(const C&) { cout << "Copy Constructor" << endl; }
  ~C() { cout << "Destructor" << endl; }
};

C foo() {
  C c1, c2;
  if (true) {
    return c1;
  } else {
    return c2;
  }
}

int main() {
    foo();
    return 0;
}

可以发现,在上面的代码中,如果编译器足够聪明,它应该能推断出foo函数只会返回c1,所以可以对foo函数应用返回值优化,那么让我们来实际编译运行一下,看看执行情况。

$ clang++ -std=c++11 -O3 foo.cpp 
$ ./a.out 
Constructor
Constructor
Copy Constructor
Destructor
Destructor
Destructor

$ g++ -std=c++11 -O3 foo.cpp 
$ ./a.out 
Constructor
Constructor
Copy Constructor
Destructor
Destructor
Destructor

可以看到,clang 5.0.0和gcc 6.3.0即使把优化选项开到最大也不能对foo函数进行返回值优化。

返回值优化还带来了一个问题,那就是拷贝构造函数和析构函数的调用变得不可预测。如果在拷贝构造函数和析构函数中存在有副作用(side-effect)的语句,就会造成不同的编译器配置编译出来的程序运行结果不一致。事实上,这是被C++11标准所允许的,下面这段话引用自C++11标准Sec 12.8 Copying and moving class objects [class.copy]

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects.

是否调用拷贝构造函数和析构函数的决定交给了编译器实现去判断,所以在实际工程中为了写出可移植的代码,就需要避免在构造函数和析构函数中加入有副作用的语句,并且应该尽量把复杂的逻辑剥离出来,放在类的其它成员函数中实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值