C++ return value optimization

Aurelius picture Aurelius · Oct 18, 2013 · Viewed 9k times · Source

This code:

#include <vector>

std::vector<float> getstdvec() {
    std::vector<float> v(4);

    v[0] = 1;
    v[1] = 2;
    v[2] = 3;
    v[3] = 4;

    return v;
}

int main() {
    std::vector<float> v(4);

    for (int i = 0; i != 1000; ++i)
    {
        v = getstdvec();
    }
}

My incorrect understanding here is that the function getstdvec shouldn't have to actually allocate the vector that it's returning. When I run this in valgrind/callgrind, I see there are 1001 calls to malloc; 1 for the initial vector declaration in main, and 1000 for every loop iteration.

What gives? How can I return a vector (or any other object) from a function like this without having to allocate it every time?

edit: I'm aware I can just pass the vector by reference. I was under the impression that it was possible (and even preferable) to write a function like this that returns an object without incurring an unnecessary allocation.

Answer

Daniel Frey picture Daniel Frey · Oct 18, 2013

When you call a function, for a return type like std::vector<T> the compiler provides memory for the returned object. The called function is responsible for constructing the instance it returns in this memory slot.

The RVO/NRVO now allows the compiler to omit creating a local temporary object, copy-constructing the returned value in the memory slot from it, destructing the temporary object and finally returning to the caller. Instead, the called function simply constructs the local object in the return slot's memory directly and at the end of the function, it just returns.

From the caller's perspective, this is transparent: It provides memory for the returned value and when the function called returned, there is a valid instance. The caller may now use this object and is responsible for calling the destructor and freeing the memory later on.

This means that the RVO/NRVO only work for when you call a function to construct a new instance, not when you assign it. The following is an example of where RVO/NRVO could be applied:

std::vector<float> v = getstdvec();

but you original code uses a loop and in each iteration, the result from getstdvec() needs to be constructed and this temporary is assigned to v. There is no way that the RVO/NRVO could remove this.