C++ 返回值优化详解 RVO与NRVO


先举一个简化的类,该类只有一个private成员,并且有无参构造函数、有参构造函数、拷贝构造函数、移动构造函数、析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Str.h
class Str
{
private:
    int size;
public:
    Str() { cout << "调用无参构造函数"; }
    Str(int n) :size(n) { cout << "调用有参构造函数"; }
    Str(const Str& s) :size(s.size) { cout << "调用拷贝构造函数"; }
    Str(Str&& ss) :size(ss.size)noexcept { cout << "调用移动构造函数"; }
    ~Str()
    {
        cout << "调用析构函数";
    }

};

1.RVO(Return Value Optimization)

  • 是编译器的一种优化机制,目的是消除为了保存函数返回值而产生的临时对象

假设现在有这样一个main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include "Str.h"

Str get_object()
{
    Str object(3);
    return object;
}
int main()
{
    Str target=get_object();
    return 0;
}

那么运行这个程序,理论上会

调用一次有参构造函数(构造局部对象object),并且会调用两次移动构造函数(如果没有定义移动构造函数,则是两次拷贝构造函数),分别是:

①在get_object()函数返回的时候,用局部对象object去初始化一个临时对象

②在main函数中,用这个临时对象初始化target

并且会调用三次析构函数,一是析构局部对象,二是析构临时对象,三是析构对象target

(关于拷贝构造函数的调用时机,请看:构造函数)

输出应该如下:
在这里插入图片描述
但是在vs2015中运行,结果却是:竟然少了一次构造和析构
在这里插入图片描述
这就是编译器进行了返回值优化,实际上编译器将get_object()函数优化成

1
2
3
4
5
6
7
8
9
10
11
12
void get_object(Str& temp)
{
    Str object(3);//构造局部对象
    temp.Str::Str(object);//调用移动构造函数,因为形参是一个局部变量
    return;
}
int main()
{
    //Str target;
    get_object(target);//将要构造的对象作为形参,传引用给优化后的函数
    return 0;
}

观察发现,优化后get_object()返回值为void,并且多一个参数,是对应类的引用;然后main函数中直接传要构造对象的引用。经过这种转换,去除了临时对象的构造,即少了一次移动构造函数和析构函数
在这里插入图片描述
(因为局部对象要在target构造完成后,才能析构,所以执行顺序与前面理论上的略有不同)

2.NRVO(Named Return Value Optimization)

  • 是一种比RVO更进一步的优化技术,同样目的是消除为了保存函数返回值而产生的临时对象

编译器将函数优化为:

1
2
3
4
5
6
7
8
9
10
11
12
void get_object(Str& temp)
{
    //直接替换局部对象,并且用局部对象的参数来构造target
    temp.Str::Str(3);  //无Str object(3);
    return;
}
int main()
{
    //Str target;
    get_object(target);//将要构造的对象作为形参,传引用给优化后的函数
    return 0;
}

显然,直接将要初始化的对象替代掉局部对象进行操作,这种情况下只需一次有参构造和析构!!!!
在这里插入图片描述
在linux中使用g++ 4.8编译即得上述结果,因为g++默认使用NROV,在vs项目设置中调整优化程度也能达到一样的效果,如果关闭g++的优化(使用参数 -fno-elide-constructors)就会得到文中开头理论的结果:
在这里插入图片描述

另外,如果get_object()函数为

1
2
3
4
5
6
7
8
9
Str get_object()
{
    return Str(3);
}
int main()
{
    Str target=get_object();
    return 0;
}

同样会进行类似NRVO的操作

1
2
3
4
5
6
7
8
9
10
11
void get_object(Str& temp)
{
    temp.Str::Str(3);
    return;
}
int main()
{
    //Str target;
    get_object(target);//将要构造的对象作为形参,传引用给优化后的函数
    return 0;
}

总结:
NRVO比RVO优化得更狠,也更有效。特别是当函数是在返回值中构造对象的时候,如return Str();比起{Str object(3); return obj};虽然在调用(移动)构造函数没有效率上的区别,都是用NROV(ROV)。但是如果是调用(移动)赋值运算符,如{Str target; target=get_object();}此时ROV就会失效,只能使用NROV