C++返回值优化

发现

#include <iostream>
using namespace std;

class Test {
  public:
    Test() { cout << this << " default constructor" << endl; }
    Test(const Test &that) { cout << this << " copy constructor" << endl; }
    ~Test() { cout << this << " deconstructor" << endl; }
};

Test func() {
    Test temp;
    return temp;
}

int main(int argc, char const *argv[]) {
    Test a = func();
    return 0;
}

上述代码的调用流程应该是这样的:

  1. 调用temp 的构造函数
  2. 函数返回值会调用临时对象复制构造函数
  3. 调用a的复制构造函数
  4. 调用返回的临时对象的析构函数
  5. 调用a的析构函数

但实际的输出却是这样的:

0x61fe0f default constructor
0x61fe0f deconstructor

经过一步步调试发现,其只进行了上述调用流程的第一步和最后一步。

为什么呢?查询资料后发现,这是C++的一项编译优化技术,称为返回值优化,即并不进行返回值构造临时对象这一过程,这样也就不会调用复制构造函数了。

若需要,我们可以加上-fno-elide-constructors这一编译选项来关闭这一优化(该选项的详细说明可以在Linux终端通过man gccman g++命令来查看)。

在终端输入以下命令:

g++ -g -o test -fno-elide-constructors test.cpp
./test

我们可以得到以下输出:

0x61fbaf default constructor
0x61fd3f copy constructor
0x61fbaf destructor
0x61fd3e copy constructor
0x61fd3f destructor
0x61fd3e destructor

该输出符合上述调用流程。

另外,函数返回值其实是优先调用移动构造函数的。若定义了移动构造函数,它将调用移动构造函数而非复制构造函数。当然,若我们自己并未定义移动构造函数,它也就只能调用复制构造函数了。

我们在代码中加入移动构造函数:

Test(Test &&that) { cout << this << " move constructor" << endl; }

并关掉返回值优化,则运行结果为:

0x61fbaf default constructor
0x61fd3f move constructor
0x61fbaf destructor
0x61fd3e move constructor
0x61fd3f destructor
0x61fd3e destructor

实现

栈帧提前构造

对于返回值优化,一种实现方法是在调用函数(caller)在栈帧上声明一个隐藏对象,然后将其地址隐蔽地传入被调用函数(callee),被调用函数的返回值直接构造到该地址上。如下所示:

#include <iostream>
using namespace std;

class Test {
  public:
    Test() { cout << this << " default constructor" << endl; }
    Test(const Test &that) { cout << this << " copy constructor" << endl; }
    // Test(Test &&that) { cout << this << " move constructor" << endl; }
    ~Test() { cout << this << " destructor" << endl; }
};

Test* func(Test* hidden) {
    Test temp;
    *hidden = temp;  
    return hidden;
}

int main(int argc, char const *argv[]) {
    Test hidden;
    Test a = *func(&hidden);
    return 0;
}

注意:由于第14行是需要调用赋值运算符的,所以我们这里将移动构造函数注释了。(移动构造函数与赋值运算符不能共存)

输出如下:

0x61fd3f default constructor
0x61fc7f default constructor
0x61fc7f destructor
0x61fd3e copy constructor
0x61fd3e destructor
0x61fd3f destructor

可见这种方法优化并不明显,仅是将一个复制构造函数换成了普通构造函数。

命名返回值优化

另一方法是命名法返回值优化,其去除了基于栈的构造与析构,而是直接将返回值移进目标对象的地址。如下所示:

#include <iostream>
using namespace std;

class Test {
  public:
    Test() { cout << this << " default constructor" << endl; }
    Test(const Test &that) { cout << this << " copy constructor" << endl; }
    // Test(Test &&that) { cout << this << " move constructor" << endl; }
    ~Test() { cout << this << " destructor" << endl; }
};

void func(Test *target) {
    Test temp;
    *target = temp;
}

int main(int argc, char const *argv[]) {
    Test a;
    func(&a);
    return 0;
}

输出如下:

0x61fd3f default constructor
0x61fc8f default constructor
0x61fc8f destructor
0x61fd3f destructor

这一方法的效率已经很高了,但还是与实际编译器的输出略有不同,或许实际中编译器或许进行了更多更复杂的优化吧。

参考


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!