14

In the following C++11+ code which return statement construction should be preferred?

#include <utility>


struct Bar
{
};

struct Foo
{
    Bar bar;

    Bar get() &&
    {
        return std::move(bar); // 1
        return bar;            // 2
    }
};
Constructor
  • 7,273
  • 2
  • 24
  • 66
  • 1
    I don't see any reason for this method to move value. I would suggest to make it a separate method with appropriate name because `get` methods are never expected to work only once (r-value qualifier of method plays no role in this matter). – user7860670 Jan 28 '18 at 11:46
  • 4
    @VTT - It does play a big role, matter of fact. If you `get` on a functions return value, you most certainly expect to call it only once on that temporary object. Why are you suggesting willful pessimization of performance? – StoryTeller - Unslander Monica Jan 28 '18 at 11:47
  • 1
    @VTT The reason is to prevent copying. The name of the method doesn't matter here, it is a simple example code. – Constructor Jan 28 '18 at 11:48

3 Answers3

19

Well, since it's a r-value ref qualified member function, this is presumably about to expire. So it makes sense to move bar out, assuming Bar actually gains something from being moved.

Since bar is a member, and not a local object/function parameter, the usual criteria for copy elision in a return statement don't apply. It would always copy unless you explicitly std::move it.

So my answer is to go with option number one.

StoryTeller - Unslander Monica
  • 165,132
  • 21
  • 377
  • 458
  • 4
    Also depending on the use case you should probably add overloads for `&` and `const &` – sbabbi Jan 28 '18 at 12:00
  • 1
    I would assume the compiler to elide the copy in option 2 and would prefer that since option 1 might prevent the elision by forcing a move. Or am I completely off my rocker? – Jesper Juhl Jan 28 '18 at 13:25
  • Could some elision be applied here in accordance with the standard? That is the question. I mean the special case of the r-value ref-qualified method. – Constructor Jan 28 '18 at 13:40
  • @Constructor - Not according to the standard as it is written today. Any access for data via `this` obtains an lvalue. And the special section that deals with copy elision for lvalues doesn't address this. Maybe there could be a proposal for it. But nowadays you must write the `std::move` explicitly. – StoryTeller - Unslander Monica Jan 28 '18 at 14:08
  • @JesperJuhl - You are thinking about [this section](https://timsong-cpp.github.io/cppwp/n4659/class.copy.elision). It doesn't apply to data members. But under the as-if rule, things may be done, I suppose. I'm not sure how common it is however. [Here's GCC 8](https://wandbox.org/permlink/syh9cMH7uyuFIR29), not being too clever at -O2. – StoryTeller - Unslander Monica Jan 28 '18 at 14:29
  • And neither is [Clang 5](https://wandbox.org/permlink/xS29OhWqpzFl2W2t), same story. – StoryTeller - Unslander Monica Jan 28 '18 at 14:30
  • @StoryTeller: The reason C++ has explicit rules for elision is that they are not allowed by the as-if rule. – Ben Voigt Jan 28 '18 at 20:46
  • @BenVoigt - How so? If the copy taking place has no observable behavior, what's to stop an implementation from eliding it? – StoryTeller - Unslander Monica Jan 29 '18 at 08:04
  • @StoryTeller: You're not talking about eliding then, but inlining... i.e. the copy constructor and destructor were called but were trivial and resulted in no code. Eliding is not conditioned on lack of observable behavior. – Ben Voigt Jan 29 '18 at 15:55
  • @BenVoigt usually "elision" doesn't necessarily refer to the "copy-elision" term of allowing omission of the copy even in presence of side effects, but may also refer to the common-english term. Folks use it to refer to "guaranteed copy elision" of C++17 aswell, even if in that case it's not the "copy-elision" term of the standard either. So I guess his is fair-usage. – Johannes Schaub - litb Jan 31 '18 at 12:22
  • @JohannesSchaub-litb: Eliding is modifying the sequence by leaving something out. But StoryTeller is talking about a case where no observable behavior is left out -- there was none to begin with. Skipping over (nothing) isn't a meaningful usage of "skip", "elide", or any other action verb. Furthermore, we are talking about Jesper's comment "I would assume the compiler to elide the copy", that is, C++ copy elision is exactly what is being discussed here. – Ben Voigt Jan 31 '18 at 19:11
  • @BenVoigt I'm almost certain he wasn't saying "eliding the nothing". But about eliding a function call or something similar. C++ copy elision is not necessarily being talked about. Only if he still says so after you confront him with the exact rules. Eliding the copy first means to .. just don't do the copy. For whatever reasons, maybe because of as-if, or maybe because of the copy-elision rule. – Johannes Schaub - litb Jan 31 '18 at 19:29
  • @BenVoigt For example, the first google result: https://stackoverflow.com/a/18610251/34509 talks about "elision of the default constructor .... based on the general as-if rule.." . Also NicolBolas and others say on that question "It is theoretically possible that a trivial copy/move assignment operator could be elided. But implementations are not allowed to elide anything that you could detect being elided." or similar. – Johannes Schaub - litb Jan 31 '18 at 19:39
4

I prefer option 3:

Bar&& get() &&
// ^^
{
    return std::move(bar);
}

and, while we're at it:

Bar& get() & { return bar; }
Bar const& get() const& { return bar; }
Bar const&& get() const&& { return std::move(bar); }

We're an rvalue, so it should be free to cannibilize our resources, so move-ing bar is correct. But just because we're open to moving bar doesn't mean we have to mandate such a move and incur extra operations, so we should just return an rvalue reference to it.

This is how the standard library do - e.g. std::optional<T>::value.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • This is especially good when the objects are not cheap to move (or moving is not desired for other reasons, eg. side effects, exceptions. All of these happen rarely, but they do.). In general - "don't pay for what you don't need". – Sopel Jan 29 '18 at 16:24
  • This is too dangerous for things like `for (auto e : Foo{}.get()) { /* ... */ }`, isn't it? – Constructor Jan 29 '18 at 16:43
  • @Constructor Yes, as with any function that returns a reference, it is possible to end up with code that has a dangling reference. – Barry Jan 29 '18 at 16:50
  • @Barry But the danger is higher if the object is a prvalue. And worth noting, if you use this rvalue-ref return type, then the only scenario that you can use your `get()` function and for which it has a benefit, is `.get().somethingImmediatelyHere()` since here it will avoid a move. For the other case, `auto f = .get();` your function will have a move aswell. – Johannes Schaub - litb Jan 30 '18 at 16:50
1

I would like to clarify my point (from comments). Even though moving result should in general be considerably more efficient than copying, it is not my primary concern here. The core issue arises from false assumption that by calling this method on r-value reference to Foo instance caller's intentions include creation of a new Bar value. For example:

Foo Produce_Foo(void);

// Alright, caller wanted to make a new `Bar` value, and by using `move`
// we've avoided a heavy copy operation.
auto bar{Produce_Foo().get()};

// Oops! No one asked us to make a useless temporary...
cout << Produce_Foo().get().value() << endl;

The solution would be to add a dedicated functions to be used just to take a peek at stored bar and to take control over content of stored bar object.

Bar const & get_bar() const noexcept
{
    return bar;
}

// no particular need to this method to be r-value reference qualified
// because there is no direct correlation between Foo instance being moved / temp
// and intention to take control over content of stored bar object.
Bar give_bar() noexcept
{
    return ::std::move(bar);
}

Now that user has a choice there will be no more problems:

// Alright, caller wanted to make a new `Bar` value, and by using `move`
// we've avoided a heavy copy operation.
// There is also no need to figure out whether Produce_Foo returned an rvalue or not.
auto bar{Produce_Foo().give_bar()};

// Alright, no extra temporaries.
cout << Produce_Foo().get_bar().value() << endl;

As for use cases for r-value reference qualified methods, I think they are mostly useful when dealing with temporaries of the same type as this object. e.g. string class implementing such concatenation operator can reduce amount of reallocations, essentially performing like a dedicated string builder.

user7860670
  • 35,849
  • 4
  • 58
  • 84
  • 4
    Is your issue with `Produce_Foo().get().value()` only that a temporary is created via move constructor when it's not necessary? I don't see any way it would be unsafe. And since most move constructors are pretty quick, this sounds like uglifying the code for a small premature optimization. – aschepler Jan 28 '18 at 13:13
  • @aschepler What do you mean by unsafe? All the C++ code is unsafe (as opposite to safe managed code). And regardless of selected method implementation these code snippets should behave correctly. Move constructors are typically pretty quick only compared to copy constructors, you gotta be careful calling removal of temporaries a small premature optimization or "uglifying the code". The point of this answer is not demonstrate that use of dedicated methods clarifies everything and that this is not the best use case for an r-value reference qualifier on method. – user7860670 Jan 28 '18 at 13:37
  • 1
    By "unsafe" I just meant undefined behavior or unintended behavior. (No further comment on the rest.) – aschepler Jan 28 '18 at 13:52