49

In several places I've seen the recommended signatures of copy and move constructors given as:

struct T
{
    T();
    T(const T& other);
    T(T&& other);
};

Where the copy constructor takes a const reference, and the move constructor takes a non-const rvalue reference.

As far as I can see though, this prevents me taking advantage of move semantics when returning const objects from a function, such as in the case below:

T generate_t()
{
    const T t;
    return t;
}

Testing this with VC11 Beta, T's copy constructor is called, and not the move constructor. Even using return std::move(t); the copy constructor is still called.

I can see how this makes sense, since t is const so shouldn't bind to T&&. Using const T&& in the move constructor signature works fine, and makes sense, but then you have the problem that because other is const, you can't null its members out if they need to be nulled out - it'll only work when all members are scalars or have move constructors with the right signature.

It looks like the only way to make sure the move constructor is called in the general case to have made t non-const in the first place, but I don't like doing that - consting things is good form and I wouldn't expect the client of T to know that they had to go against that form in order to increase performance.

So, I guess my question is twofold; first, should a move constructor take a const or non-const rvalue reference? And second: am I right in this line of reasoning? That I should stop returning things that are const?

Ben Hymers
  • 25,586
  • 16
  • 59
  • 84
  • 2
    Well, I think the use cases of returning const temporaries are somewhat small. If you declare it in advance, then because you want to do some things with it before returning. If not, then a simple `return T()` would do, anyway. Though I can still see certain use cases, I think they are rare. And of course, an rvalue reference from which you cannot steal resources is not really of any value compared to an lvalue reference. Interresting question, though. – Christian Rau May 26 '12 at 22:25
  • 1
    By the way, isn't this code eligible for NRVO? So what "should" happen is that the `const` object `t` is constructed into the same location as the non-const return value. Then if necessary that non-const return value *can* be moved by the caller via the non-const move constructor (or a move assignment operator). In the case of construction, though, it would again be eligible for copy elision and `t` could be constructed directly into whatever's being initialized using a call to `generate_t()`. What's obstructing VC11 from making this optimization? – Steve Jessop May 26 '12 at 22:48
  • 3
    _consting things is good form_ not if you want to modify them, e.g. by moving from them. – Jonathan Wakely May 26 '12 at 22:49
  • ... the upshot of which is that until someone explains how I've failed to understand the interaction of moves with copy elisions, I believe you're right that the client of `T` *doesn't* need to go against that form in order to increase performance. However as a QoI issue, VC11 (with the options you gave it) has dropped the ball. Still, in general I don't think it's quite correct to say that the client of a class *shouldn't* need to think about which objects can be moved from in order to increase performance. It's just that in this case I don't think they do. – Steve Jessop May 26 '12 at 22:54
  • @SteveJessop, it is eligible for copy elision. It can't be moved, because it's const so the move constructor isn't viable, so the copy constructor is chosen but the copy is elided. If the variable was non-const (or the move constructor took `const T&&` **which would be bad**) then the move constructor would be viable, but would be elided. So the only difference should be whether a move or copy is elided, but it should be elided either way. – Jonathan Wakely May 26 '12 at 23:04
  • @Jonathan: The return value is *not* const. Therefore it can validly be moved from. It doesn't matter whether the object that was elided is `const` or not. How would the compiler even know when compiling the TU containing the call to `generate_t()`, whether or not the implementation of the function happens to involve a copy elision from a const object? The rules on copy elision say that the *cv-unqualified* types of the objects must match, not that the destination of the elided copy suddenly becomes a const object. – Steve Jessop May 26 '12 at 23:06
  • @Steve, yep, I was talking about the copy of `t` to the return value. If the return value is used to initialize another T then that would be eligible for move, and that move can be elided. You can test that by defining the move ctor as deleted, then the `const T` can be copied to the return value, but the return value can't be moved to initialize a new value (doing so would want to use the deleted move ctor, even if the move would actually be elided). Similarly, if you define the copy ctor as deleted you see that `const T t` can't be returned, because that return is a copy (even if elided). – Jonathan Wakely May 26 '12 at 23:18
  • @ChristianRau - good point. The only reason I do this is that I'm often writing code on the Xbox, which makes it very difficult to observe return values when debugging. Declaring the variable then returning it makes this much easier, and making that variable const is natural to me. – Ben Hymers May 27 '12 at 14:50
  • @BenHymers Though I unerstand the part debugging has in achieving good productivity, I am 100% convinced, that peripheral things like debugging shouldn't have any impact on coding style. Especially debugging shouldn't by any means be a reason to transform code with perfect immutable value semantics, as `return T(...)` surely is, into something that ugly as `T t(...); return t;`, which is just plain ugly compared to the first version, no matter if const or not. – Christian Rau May 27 '12 at 21:45

4 Answers4

40

It should be a non-const rvalue reference.

If an object is placed in read-only memory, you can't steal resources from it, even if its formal lifetime is ending shortly. Objects created as const in C++ are allowed to live in read-only memory (using const_cast to try to change them results in undefined behavior).

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • 4
    Chances are this is not the case. An object from which you can move seems to indicate that it manages resources, so the memory must be writable when the object is constructed, and it probably has a destructor that releases those resources, so again, it would have to be writable during destruction. I doubt that the compiler will generate code to manage marking the memory page as read-only after construction and then mark it writable before destruction (and read-only again after destruction completes as most probably the object is not alone in that memory page...) That being said +1 – David Rodríguez - dribeas May 26 '12 at 22:38
  • @David: "I doubt that..." - true, but it might be useful feature in debug mode, esp. when fixing legacy const-incorrect code. I think the important point is that although this thing won't actually happen, the fact that it's legal explains why you can't modify const objects prior to their destructor (and in particular can't efficiently move from them where they hold resources). – Steve Jessop May 26 '12 at 22:41
  • @SteveJessop: That is the reason for the +1: whether the object is in read-only memory or not does not change the fact that moving out of a constant object is breaking the contract: you are modifying something that you promised not to touch. – David Rodríguez - dribeas May 26 '12 at 22:43
  • @DavidRodríguez-dribeas: I don't see why releasing resources during destruction requires writing to the object holding those resources. That said, destructors are called for `const` objects and it's legal to modify the object inside its destructor, so there would be a potential need to unprotect the memory. I still suspect that the compiler could usually elide the unprotection, writing to members just before they become inaccessible seems like it would be unusual. – Ben Voigt May 26 '12 at 22:48
10

A move constructor should normally take a non-const reference.

If it were possible to move from a const object it would usually imply that it was as efficient to copy an object as it was to "move" from it. At this point there is normally no benefit to having a move constructor.

You are also correct that if you have a variable that you are potentially going to want to move from then it will need to be non-const.

As I understand it this is the reason that Scott Meyers has changed his advice on returning objects of class type by value from functions for C++11. Returning objects by const qualified value does prevent unintentionally modification of a temporary object but it also inhibits moving from the return value.

CB Bailey
  • 755,051
  • 104
  • 632
  • 656
  • 3
    The following statement is not obvious or unclear what it means: *If it were possible to move from a const object it would usually imply that it was as efficient to copy an object as it was to "move" from it* – Patrick Fromberg Mar 25 '18 at 23:10
9

Should a move constructor take a const or non-const rvalue reference?

It should take non-const rvalue reference. The rvalue references first of all don't make sense in their const forms simply because you want to modify them (in a way, you want to "move" them, you want their internals for yourself ).

Also, they have been designed to be used without const and I believe the only use for a const rvalue reference is something very very arcane that Scott Meyers mentioned in this talk (from the time 42:20 to 44:47).

Am I right in this line of reasoning? That I should stop returning things that are const?

This is a bit of too general question to answer I reckon. In this context, I think it's worth mentioning that there's std::forward functionality that will preserve both rvalue-ness and lvalue-ness as well as const-ness and it will also avoid creating a temporary as a normal function would do should you return anything passed to it.

This returning would also cause the rvalue reference to be "mangled" into lvalue reference and you generally don't want that, hence, perfect forwarding with the aforementioned functionality solves the issue.

That being said, I suggest you simply take a look at the talk that I posted a link to.

Enlico
  • 23,259
  • 6
  • 48
  • 102
ScarletAmaranth
  • 5,065
  • 2
  • 23
  • 34
5

In addition to what is said in other answers, sometimes there are reasons for a move constructor or a function to accept a const T&&. For example, if you pass the result of a function that returns a const object by value to a constructor, T(const T&) will be called instead of T(T&&) as one would probably expect (see function g below).

This is the reason behind deleting overloads that accept constT&& for std::ref and std::cref instead of those that accept T&&.

Specifically, the order of preference during overload resolution is as follows:

struct s {};

void f (      s&);  // #1
void f (const s&);  // #2
void f (      s&&); // #3
void f (const s&&); // #4

const s g ();
s x;
const s cx;

f (s ()); // rvalue        #3, #4, #2
f (g ()); // const rvalue  #4, #2
f (x);    // lvalue        #1, #2
f (cx);   // const lvalue  #2

See this article for more details.

Dev Null
  • 4,731
  • 1
  • 30
  • 46