16

Consider the following:

struct A { /* ... */ };

A foo() {
  auto p = std::make_pair(A{}, 2);
  // ... do something
  return p.first;
}

auto a = foo();

Will p.first be copied, moved or RVO-ed?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Nubcase
  • 788
  • 4
  • 15
  • That may depend on compiler implementation, but in general I'd expect RVO. – πάντα ῥεῖ Oct 26 '15 at 11:14
  • 2
    @πάνταῥεῖ I don't think it's allowed if an expression from a return statement is anything else than a plain identifier – Piotr Skotnicki Oct 26 '15 at 11:24
  • @PiotrSkotnicki Good point, seems I've missed that. – πάντα ῥεῖ Oct 26 '15 at 11:42
  • 1
    I like guarantees, and as far as I know, in general, RVO is not guaranteed by the standard, only allowed, right? So I prefer using e.g. non-const reference args, pointer args or smart pointers to be 100% sure no unnecessary copying is done. – Erik Alapää Oct 26 '15 at 11:53
  • @PiotrSkotnicki: That's not true. There are several other cases where it's allowed, most importantly unnamed objects. – Christian Hackl Oct 26 '15 at 12:56
  • If you want RVO you could consider `std::tie` e.g. `int n; A a; std::tie(a, n) = func(); return a;` – Yankes Oct 26 '15 at 13:50
  • @ErikAlapää making the class movable ensures no unnecessary copies – M.M Oct 26 '15 at 19:56
  • @erik RVO is more than allowed by the standard. It is actively encouraged. – Richard Hodges Oct 26 '15 at 19:58
  • @M.M: The rvalue refs and moveability is a great addition to the language, but I still am not 100% comfortable with using the new constructs, since I do not understand them fully. Also, last time I looked at move, there still were no 100% guarantee that no copies are ever made, at least in some cases. So, since I am old school and want to know exactly what my code does, I will probably stick with non-const reference or pointer args to get 0 copy in all cases. – Erik Alapää Oct 27 '15 at 04:43
  • Maybe learn about it some more, it is 100% guarantee class is moved in this case, if it is movable. – M.M Oct 27 '15 at 06:13
  • I mean in general. I want guarantees from the standard about copies not being made. If I do it through pointer args, I know what happens and get high performance. Also, the standard legalese in general is quite impenetrable and wordy, even for me who have coded C++ since 1992. Maybe 'normative examples' in the standard would help working programmers and compiler writers understand it. – Erik Alapää Oct 27 '15 at 19:48

4 Answers4

10

I've found in Visual Studio 2010 and in gcc-5.1 RVO is not applied (see for example http://coliru.stacked-crooked.com/a/17666dd9e532da76).

The relevant section of the standard is 12.8.31.1 [class.copy]. It states that copy elision is permitted (my highlighting):

in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function parameter or a variable introduced by the exception-declaration of a handler ([except.handle])) with the same type (ignoring cv-qualification) as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function's return value

Since p.first is not the name of an object, RVO is prohibited.

atkins
  • 1,963
  • 14
  • 27
  • 2
    p.first is absolutely not the name of an object. :-) – Richard Hodges Oct 26 '15 at 11:48
  • Hmm does that mean it works if you first `auto& r = p.first`? Does that create a new "name" in this sense? – Lightness Races in Orbit Oct 26 '15 at 12:33
  • @LightnessRacesinOrbit `r` is the name of a reference, not the object itself. So it probably does not work either. – rozina Oct 26 '15 at 12:41
  • 2
    @rozina: Actually it functions as a name for the referent. This is the whole purpose of references, as distinct from pointers. _But_ I'm not sure how much of that is according to standard terminology as opposed to being English terminology derived from usage. – Lightness Races in Orbit Oct 26 '15 at 12:43
  • @LightnessRacesinOrbit One could be pedantic and say it functions as an alias for the object. An object can have only one name but many aliases :) Would be interesting to see how the standard define it indeed. – rozina Oct 26 '15 at 12:47
  • 2
    @LightnessRacesinOrbit gcc-5.1 considers returning a reference *even to a simple local object* is enough to switch off RVO (http://coliru.stacked-crooked.com/a/84e5744d0a68f6dd). Interesting! – atkins Oct 26 '15 at 12:50
  • @rozina: What's an alias if not another name for something? – Lightness Races in Orbit Oct 26 '15 at 12:54
  • @LightnessRacesinOrbit Based on English language it is a "a false or assumed identity" or "a fake name". It is ofc another name. But not the original name. I would assume "name of a object" in terms of the standard would only mean the original name and not aliases. – rozina Oct 26 '15 at 12:56
  • @rozina: There are plenty of definitions of the ilk "assumed name", "alternative name". – Lightness Races in Orbit Oct 26 '15 at 12:57
  • @atkins: Curiously, MSVC 2013 also turns off RVO in this case. – Christian Hackl Oct 26 '15 at 12:58
  • 2
    In 8.3.2.1 [dcl.ref], there is this "Note": *A reference can be thought of as a name of an object.* Do we take that as "can be thought of, but in fact is not"?! – atkins Oct 26 '15 at 13:00
  • 1
    @atkins: If the C++ standard did not want us to think of references as names for objects, then it would not include this informative note. – Christian Hackl Oct 26 '15 at 13:07
  • 1
    This discussion could probably be a new question – M.M Oct 26 '15 at 20:03
  • [basic]/4 says "A *name* is a use of an identifier [...] that denotes an entity or label". `r` seems to fit those criteria – M.M Oct 26 '15 at 20:03
  • @LightnessRacesinOrbit I think "name of..." intends to refer to a name lookup result, rather than to an object identity establishment at runtime (when the expression is evaluated). Consider that the reference might also refer to an object of static storage duration.. figuring out *that* at compile time is not computable (halting problem), nor accessible (may depend on user input). – Johannes Schaub - litb Oct 27 '15 at 14:49
  • Worth noting that `p.first` refers to an object (*not* a variable, *but* referring to an object), and `first` is a name (obviously). And `p.fist` refers to an object of automatic storage duration. But still, it appears that it is not "the name of an automatic object". Such silliness happen in the C++ draft. Probably because `p.first` is not a "name", which is pretty subtle. Consider a function like `T f() { return nonStaticDataMember; }`. This appears to be a name according clause 3, but it isn't actually, because of the implicit `(*this).` before it added by clause 9. – Johannes Schaub - litb Oct 27 '15 at 14:52
  • 1
    @ᐅJohannesSchaub-litbᐊ Ok time for a fresh question! http://stackoverflow.com/q/33371684/560648 – Lightness Races in Orbit Oct 27 '15 at 15:13
  • @atkins: See above plz – Lightness Races in Orbit Oct 27 '15 at 15:13
  • @ChristianHackl: See above plz – Lightness Races in Orbit Oct 27 '15 at 15:14
  • @RichardHodges No expression is the name of an object. – curiousguy Oct 28 '15 at 13:23
10

Just to add a little more fuel, how would this function if RVO were in play? The caller has put an instance of A somewhere in memory and then calls foo to assign to it (even better, let's assume that that A was a part of a larger struct, and let's assume that it is correctly aligned such that the next member of the struct is immediately after that instance of A). Assuming RVO were in play, the first portion of p is located where the caller wanted it, but where does the int that is second get placed? It has to go right after the instance of A in order to keep the pair functioning correctly, but at the source location, there's some other member right after that instance of A.

I would expect that RVO would not be happening in this place as you are only returning a portion of a larger object. A move could happen as first would have to be left in a destructible state.

Andre Kostur
  • 770
  • 1
  • 6
  • 15
3

@atkins got here first with the answer. Just adding this little test program which you may find useful in future when tracking move/assign behaviour.

#include <iostream>
#include <string>

using namespace std::string_literals;

struct A {
    A()
    : history("created")
    {
    }

    A(A&& r)
    : history("move-constructed,"s + r.history)
    {
        r.history = "zombie: was "s + r.history;
    }
    A(const A& r)
    : history("copied from: " + r.history)
    {
    }
    ~A() {
        history = "destroyed,"s + history;
        std::cout << history << std::endl;
    }
    A& operator=(A&& r) {
        history = "move-assigned from " + r.history + " (was "s + history + ")"s;
        r.history = "zombie: was "s + r.history;
        return *this;
    }
    A& operator=(const A&r ) {
        history = "copied from " + r.history;
        return *this;
    }
    std::string history;
};

A foo() {
    auto p = std::make_pair(A{}, 2);
    // ... do something
    return p.first;
}



auto main() -> int
{
    auto a = foo();
    return 0;
}

example output:

destroyed,zombie: was created
destroyed,move-constructed,created
destroyed,copied from: move-constructed,created
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
0

Consider following code:

struct A {};
struct B {};
struct C { B c[100000]; };

A callee()
{
    struct S
    {
        A a;
        C c;
    } s;
    return s.a;
}

void caller()
{
    A a = callee();
    // here should lie free unused spacer of size B[100000]
    B b;
}

"Partial" RVO should result in excessive stack usage bloating in caller, because (I think) S can be constructed only entirely in caller stack frame.

Another issue is ~S() behaviour:

// a.hpp
struct A {};
struct B {};
struct C { A a; B b; ~C(); };
// a.cpp
#include "a.hpp"
~C() { /* ... */; }
// main.cpp
#include "a.hpp"
A callee()
{
    C c;
    return c.a;
} // How to destruct c partially, having the user defined ~C() in another TU?
// Even if destructor is inline and its body is visible,
// how to automatically change its logic properly?
// It is impossible in general case.
void caller() { A a = callee(); }
Tomilov Anatoliy
  • 15,657
  • 10
  • 64
  • 169
  • 1) "_should result in excessive stack usage bloating in caller_" bad argument: the optimisation will not be applied when it causes ridiculous memory overhead; -1 for this one. 2) "_How to destruct c partially_" There is no such thing as partial destruction. +1 for this argument. – curiousguy Oct 28 '15 at 14:10
  • @curiousguy I revised first argument, but the answer remains partially invalid for the history :). – Tomilov Anatoliy Oct 28 '15 at 14:46