C++返回值优化与移动语义的思考

C++的class本质上是值语义的,它在作为函数返回值时可能会造成不必要的拷贝

返回值优化(RVO)

返回值优化是编译器对返回一个值语义对象时进行的优化,这有助于性能优化

假如我们有一个Foo类,它的定义如下所示

class Foo {
public:
    Foo() {
        cout << "construct()\n";
    }
    Foo(const Foo&) {
        cout << "copy()\n";
    }
    ~Foo() {
        cout << "destruct()\n";
    }
};

我们返回一个Foo对象

Foo f() {
    Foo foo;
    return foo;
}
int main() {
    Foo foo = f();
}

它的输出如下:

construct()
destruct()

可以看出,main函数中的两个对象没有经过拷贝构造

不过返回值优化仅在返回值对象确定的情况下才会使用,我们来看下面一个例子

Foo f(int n) {
    Foo foo1, foo2;
    if (n % 2) {
        return foo1;
    } else {
        return foo2;
    }
}
int main() {
    Foo foo = f(3);
}

它的输出结果如下:

construct()
construct()
copy()
destruct()
destruct()
destruct()

可以很容易看出来,这次并没有返回值优化,原因就在于编译器不清楚main函数中的foo所占有的栈空间该与哪个if/else分支的对象对应

所以在这种情况下,我们要避免这种函数的编写

移动语义

C++11的移动语义解决了上述的部分问题,比如像std::string,std::vector这种容器保存了它所占有的资源的指针,它本身的空间占用并不大(当然std::string具体实现可能有SSO优化,不过所占用的空间也是很小的

我们为Foo类添加移动构造函数

class Foo {
public:
    Foo() {
        cout << "construct()\n";
    }
    Foo(const Foo&) {
        cout << "copy()\n";
    }
    Foo(Foo&&) {
        cout << "move()\n";
    }
    ~Foo() {
        cout << "destruct()\n";
    }
};

再运行一次

输出结果:

construct()
construct()
move()
destruct()
destruct()
destruct()

我们可以发现move操作替换了copy,编译器在返回值时会首先匹配移动构造函数

这在返回std::vector,std::string之类的对象时性能提升是很大的,原因就是移动时仅转移了它们内部的资源指针。

那么我们有必要显式添加std::move(局部对象)让编译器完成移动操作呢?

运行下面代码

Foo f() {
    Foo foo;
    return std::move(foo);
}
int main() {
    Foo foo = f();
}

输出结果:

construct()
move()
destruct()
destruct()

我们从结果可推知,返回时显式对局部对象添加std::move(),会阻止编译器进行返回值优化,这样可能会造成不必要的性能损失

所以我们最好在返回一个非局部对象时才考虑使用std::move()来移动它

对C++编程的启示

由于值语义的存在,设计函数时返回一个对象对于C++程序员是有一定的心理负担的,而现代C++提供了返回值优化与移动语义来减轻程序员的负担

不过对于比较复杂的函数或者sizeof(class)比较大的情况,传递引用或者指针还是一种比较好的方式