返回值优化-消除拷贝

发布时间 2023-03-25 16:45:33作者: Clemens

我们使用gcc编译器,C++11,

《c++核心指南》的一个条款:“For “out” output values, prefer return values to output parameters”:在函数输出数值时,尽量使用返回值而非输出参数。

我们之前的做法应该是函数参数里面使用指针或者引用,而不是返回值,为什么现在推荐使用返回值?

返回非引用类型的表达式结果是个纯右值(prvalue)。在执行 Obj r = … 的时候,编译器会认为我们实际是在构造 Obj r(…),而“…”部分是一个纯右值。因此编译器会首先试图匹配 Obj(Obj&&),在没有时则试图匹配 Obj(const Obj&);也就是说,有移动支持时使用移动,没有移动支持时则拷贝。

1. 首先看一个例子:

class Obj
{
public:
    Obj() {std::cout << "Obj()" << std::endl;}
    ~Obj() { std::cout << "~Obj()" << std::endl; }
    Obj(const Obj &) {std::cout << "copy obj" << std::endl;}
    Obj(Obj &&) {std::cout << "move obj" << std::endl;}
};
Obj getObj()
{
    return Obj();
}
int main()
{
    auto a = getObj();
}

 首先思考一下,是不是觉得这个例子的输出应该有“copy obj“或者“move obj”。但其实只输出两行:

Obj()
~Obj()

这里的最主要原因是编译器给我们做了返回值优化。

 

2. 稍微变动一下代码:

Obj getObj()
{
    Obj a;
    return a;
}

输出仍然是一样。但在其他编译器下有可能会调用移动构造函数。

 

3. 继续改动一下代码:

Obj getObj(int n)
{
    Obj a1;
    Obj a2;
    if (n > 10) {
        return a1;
    } else {
        return a2;
    }
}
int main()
{
    auto a = getObj(5);
}

这次的输出是:

Obj()
Obj()
move obj
~Obj()
~Obj()
~Obj()

可以看到,这里使用了移动构造函数。

 

4. 接下来把移动构造函数删除:

class Obj
{
public:
    Obj() {std::cout << "Obj()" << std::endl;}
    ~Obj() { std::cout << "~Obj()" << std::endl; }
    Obj(const Obj &) {std::cout << "copy obj" << std::endl;}
};

可以看到输出:

Obj()
Obj()
copy obj
~Obj()
~Obj()
~Obj()

可以看到,调用了拷贝构造函数。

 

5. 再进一步,删除拷贝构造函数:

因为编译器会默认提供拷贝构造函数和移动构造函数,我们应该使用delete,不能简单的注释掉。

class Obj
{
public:
    Obj() {std::cout << "Obj()" << std::endl;}
    ~Obj() { std::cout << "~Obj()" << std::endl; }
    Obj(const Obj &) = delete;
};

可以看到,上面的函数不能正常工作。

 

编译器不是会提供默认移动构造函数吗,为什么第4步和第5步中还是调用了拷贝构造函数?

根据c++标准:如果用户声明了一个显式的拷贝构造函数,即使声明中使用了delete,都不会有隐式声明的移动构造函数被生成。也就是说,如果一个类X没有显式的声明移动构造函数,那么当且仅当用户没有提供拷贝构造函数时,才会生成默认的移动构造函数。

 

参考:

https://stackoverflow.com/questions/23771194/why-should-i-delete-move-constructor-and-move-assignment-operator-in-a-singleton