11

Let's consider these two functions :

// 1. Multiple returns of the same named object
string f() {
    string s;
    if (something())
        return s.assign(get_value1());
    else
        return s.assign(get_value2());
}

and

// 2. Multiple returns, all of unnamed objects
string g() {
    if (something())
        return get_value1();
    else
        return get_value2();
}

How each of these functions will actually behave in terms of RVO is of course compiler-dependent. Am I right, however, to assume that RVO for both of them is common ?


p.s. (See answers) Function #1 was intended to be the following:

string f() {
    string s;
    if (something())
        return s;
    s.assign(get_value());
    return s;
}
n.caillou
  • 1,263
  • 11
  • 15

1 Answers1

17

For #1, NRVO is guaranteed not to happen, that is, you're guaranteed to get a copy from s to the return value of the function. In this case, you're better off doing

return std::move(s.assign(get_value1()));

Alternatively, if possible, rewrite the function to be NRVO-friendly:

string f() {
    string s;
    if (something())
        s.assign(get_value1());
    else
        s.assign(get_value2());
    return s;
}

Before the compiler even considers NRVO, several Standard requirements have to be met. The one that is not satisfied here is that the expression in the return statement has to be the name of a variable. s.assign(...) is not a name, it is a more complicated expression; you need to have something like return s; for NRVO to be considered.

For #2, assuming the get_value functions return string (or const string), you will most likely have RVO on any modern compiler, and, if all goes well with the ratification of C++17, RVO will be guaranteed in C++17 mode in any conformant compiler (still no guarantees for NRVO).

You can find very good and comprehensive information about (N)RVO (called copy elision in the Standard) on cppreference.com.


I decided to check the current compiler status, so I did some tests on GCC 6.1.0, Clang 3.8.0 and MSVC 2015 Update 3.

For #2, you do get RVO from all three compilers (prvalues in the return statements are easy enough to analyze).

You also get NRVO from all three compilers for a construct like the "NRVO-friendly" one above (for MSVC, you need to have optimizations enabled).

However, for a function like

string f() {
    string s;
    if (something())
        return s;
    s.assign(get_value());
    return s;
}

GCC and Clang do NRVO, but MSVC doesn't; it does however generate moves from s to the return value, which is Standard conforming.

For another example:

string f() {
    string s;
    if (something())
        return get_value1();
    if (something_else())
        return get_value2();
    s.assign(get_value3());
    return s;
}

All three compilers do RVO for the first two returns and a move from s for the third one.

bogdan
  • 9,229
  • 2
  • 33
  • 48
  • Thank you for the reply. I did conceive the `get_value` as evaluating to `string`. And I got overcarried when compacting the example, what I meant for #1 would in practice be more like `string s; if(sthg()) {return s;} s.assign(get_value()); return s;`. Still a reminder that “copy elision” won't happen if I am careless. Since my question was primarily focussed on the second case, I am marking it as solved and will not edit further. – n.caillou Jul 12 '16 at 04:46
  • @n.caillou That construct does tick all the boxes for Standard requirements for copy elision, and it looks like you do get NRVO from GCC and Clang, but not from MSVC (2015 Update 3) - multiple return statements with the same variable name still confuse it. It does however generate moves from `s` to the return value, which is Standard conforming. If MSVC is important in your case, it's probably better to rewrite the function to return in one place for `return s;`. Multiple `return get_value();` are fine for all three compilers - prvalues are easier for the compiler to analyze. – bogdan Jul 12 '16 at 07:50
  • On second thought, I think I'll move this comment into the answer. – bogdan Jul 12 '16 at 07:57
  • I'm wondering what's the best approach and what happens if i want to return a pair containing another container, like `std::pair, int> foo() { std::vector vec = (/*something useful*/); return std::make_pair(vec, 42); }`? If I understood correctly, copy elision can happen for the std::pair since the return's statement is a nameless temporary but what happens for the vector? – yussuf Jul 12 '16 at 08:38
  • @yussuf There will be a copy from `vec` into the first member of the `pair` returned from `make_pair`, so `return std::make_pair(std::move(vec), 42);` will be better. Moves are comparatively cheap for `vector`s so in practice it's probably not worth going to any more trouble than this. If you can change the structure of the function and you absolutely want to avoid any copies or moves (maybe you have a `std::array` instead of the `vector`), then you can construct a local `pair` variable with the container in it, do work on the container member, and return the named pair variable. – bogdan Jul 12 '16 at 09:19
  • 1
    @yussuf Something like `std::pair, int> foo() { std::pair, int> p = {{1, 2, 3}, 42}; auto& a = p.first; /* work with a */ return p; }`. – bogdan Jul 12 '16 at 09:37