理解C++的Reture Passing

-- TOC --

在函数执行结束时,直接返回一个函数内创建的对象(not primitive type)怎么样?这是我们在Python中毫不犹豫的写法,但是在C++中,似乎就有些顾虑了。

顾虑的地方在,调用栈在函数返回后会变得不可用,默认copy一个size很大的object出来,很低效(return by value)。但,C++真的是这样的吗?

RVO

RVO:Return Value Optimization

表示在return语句内直接创建临时对象并返回的场景,此时,C++编译器已经优化为,只有最终的一次对象创建。

#include <iostream>
#include <cstdio>
#include <string>
using namespace std;


struct xyz {
    int a;

    xyz(): a{} {cout << "zero-parameter constructor\n";}
    explicit xyz(int a): a{a} {cout << "one-parameter constructor\n";}

    xyz(const xyz &x): a{x.a} {cout << "copy constructor\n";}

    xyz& operator=(const xyz &x){
        a = x.a;
        cout << "copy assignment\n"; 
        return *this;
    }

    xyz(const xyz &&x): a{x.a} {cout << "move constructor\n";}
};

xyz test1() {
    return xyz{};
}

xyz test2(int a) {
    return xyz{a};
}

int main(void) {
    cout << "--1--\n";
    xyz a {test1()};
    cout << a.a << endl;
    cout << "--2--\n";
    xyz b {test2(8)};
    cout << b.a << endl;
    cout << "--3--\n";
    xyz c {};
    c = test1();
    return 0;
}

输出:

--1--
zero-parameter constructor
0
--2--
one-parameter constructor
8
--3--
zero-parameter constructor
zero-parameter constructor
copy assignment

对象a和b的创建,都是一次到为,这就是RVO,现代C++编译器基本都支持。对象c的copy assignment部分,没有任何优化,return语句内的对象被创建,然后执行copy assignment。

NRVO

NRVO:Named Return Value Optimization

NRVO,表示return一个在函数内有name的对象,即一个local对象,在return之前已经被创建。

测试代码如下:

#include <iostream>
#include <cstdio>
#include <string>
using namespace std;


struct xyz {
    int a;

    xyz(): a{} {cout << "zero-parameter constructor\n";}
    explicit xyz(int a): a{a} {cout << "one-parameter constructor\n";}

    xyz(const xyz &x): a{x.a} {cout << "copy constructor\n";}

    xyz& operator=(const xyz &x){
        a = x.a;
        cout << "copy assignment\n"; 
        return *this;
    }

    xyz(const xyz &&x): a{x.a} {cout << "move constructor\n";}
};

xyz test3(){
    xyz a{9};
    printf("in test3, %p\n", &a);
    a.a = 10;
    return a;
}

int main(void) {
    cout << "--1--\n";
    xyz c {test3()};
    cout << c.a << endl;
    printf("in main, %p\n", &c);
    return 0;
}

输出:

--1--
one-parameter constructor
in test3, 0x7fffc6c3e21c
10
in main, 0x7fffc6c3e21c

依然只有一次对象创建,而且,神奇的地方在于,test3内创建的对象的地址,在test3 return后,还存在并且与test3内地址一致。这就是NRVO。

查阅了一点资料,说NRVO用调用者的stack空间创建对象:

The common idea of these two optimizations is to allow the compiler to use the memory space of this object t, which is outside the function, to directly construct the object being initialized inside the function and that is returned from it. This effectively removes the need for copying intermediary objects.

NRVO的优化可以通过一个编译选项关闭,-fno-elide-constructors,我们来试试效果:

$ g++ -fno-elide-constructors test.cpp
$ ./a.out
--1--
one-parameter constructor
in test3, 0x7fff084f445c
move constructor
10
in main, 0x7fff084f447c

NRVO的优化没有了,变成了我们熟悉的,创建临时对象,然后调用move constructor。(C++11的优化,return local对象,默认使用move,如果对象不支持move,就走copy)

Note though that compilers have different optimization capabilities, and there is no guarantee that the above optimizations will be applied (although this might be enforced in a future version of the standard for some cases). As a general rule, virtually all compilers apply RVO, and NRVO is applied by most compilers where the function is not too complex (and this varies from compiler to compiler).

所有编译器基本都支持RVO,这个实现起来简单。是否支持NRVO,就要看编译器以及代码复杂度了,g++没问题,我在网上看到有文章测试VS的支持有点问题。

只要对象支持move,return local对象,就是轻轻松松的操作,或者RVO,或者NRVO,或者move。

return by reference

防止copy一个大的对象,还可以return by reference。

#include <iostream>
#include <cstdio>
#include <string>
using namespace std;


struct xyz {
    int a;

    xyz(): a{} {cout << "zero-parameter constructor\n";}
    explicit xyz(int a): a{a} {cout << "one-parameter constructor\n";}
    xyz(const xyz &x): a{x.a} {cout << "copy constructor\n";}
    xyz(const xyz &&x): a{x.a} {cout << "move constructor\n";}
};

const xyz& test(xyz &x){
    x.a = 11;
    return x;
}

xyz& test2(xyz &x){
    x.a = 12;
    return x;
}

int main(void) {
    xyz a{10};
    xyz b {test(a)};  // copy
    const xyz &c {test(a)};
    cout << c.a << endl;
    xyz &d {test2(a)};
    const xyz &e {test2(a)};
    cout << e.a << endl;
    xyz f {test2(a)};  // copy
    return 0;
}

输出:

one-parameter constructor
copy constructor
11
12
copy constructor

只有对象b和f的创建,走了copy。其它的几个引用,都指向对象a。跟return指针还是有点区别,return引用的时候,caller如果不是引用,还可以走copy,这是合法的。

本文链接:https://cs.pynote.net/sf/c/cpp/202210038/

-- EOF --

-- MORE --