Teachability problems with std::move

925 views
Skip to first unread message

Geoffrey Romer

unread,
Jun 24, 2013, 2:42:04 PM6/24/13
to std-pr...@isocpp.org
It's surprisingly hard to come up with a good, teachable set of rules for when to use std::move. For example, it would be nice to be able to say "you can leave out std::move when returning a named local variable", but this is not always the case. Consider:

std::unique_ptr<const Foo> FooFactory() {
  std::unique_ptr<Foo> result(new Foo);
  // ...
  return std::move(result);
}

The "std::move" is mandatory here, because an lvalue can only be implicitly treated as an rvalue during copy operations, and in "return result;", 'result' is not copied, but used as the input for an implicit conversion. It seems like this should be fixable, perhaps by expanding the implicit-rvalue rules, but I'm not sure what all the consequences would be (at a minimum, this could change the behavior of existing code that has separate rvalue and lvalue overloads for an implicit conversion operation).

Conversely, it would be nice to be able to say "When in doubt, just use std::move if you want move semantics, because it never hurts", but std::move can in fact be a pessimization:

std::array<Foo, 10000> MakeHugeArray() {
  std::array<Foo, 10000> result;
  // ...
  return std::move(result);
}

As written, the return statement incurs 10,000 invocations of Foo's move constructor (or, worse, its copy constructor), but if std::move is eliminated, the return statement becomes effectively free, because it's an elidable copy/move. In this case it's not at all clear to me how to fix the problem; it seems like a fix would either require special-casing std::move (which seems like a hack, and breaks the convention that the core-language standard tries not to refer to the library standard), or require the implementation to 'see into' the implementation of a function called in a return statement (which seems impractical). 

I think it's worth trying to fix these issues, despite the difficulties, because being able to provide reliable "rules of thumb" would substantially improve the learning curve for std::move. Can anyone suggest how these issues could be fixed?

Richard Smith

unread,
Jun 24, 2013, 3:14:17 PM6/24/13
to std-pr...@isocpp.org
On Mon, Jun 24, 2013 at 11:42 AM, Geoffrey Romer <gro...@google.com> wrote:
> It's surprisingly hard to come up with a good, teachable set of rules for
> when to use std::move. For example, it would be nice to be able to say "you
> can leave out std::move when returning a named local variable", but this is
> not always the case. Consider:
>
> std::unique_ptr<const Foo> FooFactory() {
> std::unique_ptr<Foo> result(new Foo);
> // ...
> return std::move(result);
> }
>
> The "std::move" is mandatory here, because an lvalue can only be implicitly
> treated as an rvalue during copy operations, and in "return result;",
> 'result' is not copied, but used as the input for an implicit conversion. It
> seems like this should be fixable, perhaps by expanding the implicit-rvalue
> rules, but I'm not sure what all the consequences would be (at a minimum,
> this could change the behavior of existing code that has separate rvalue and
> lvalue overloads for an implicit conversion operation).

Yes, for NRVO, I think the "same cv-unqualified type as the function
return type" restriction only needs to apply to copy-elision and not
to implicit move. That is, I think we should treat 'result' as an
xvalue here:

T f() {
U result;
return result;
}

... independent of the types of T and U, but we should only be
permitted to perform copy-elision of they're the same cv-unqualified
type. Most generalizations of the implicit-rvalue rules are fraught
with peril, but this one seems relatively safe.

> Conversely, it would be nice to be able to say "When in doubt, just use
> std::move if you want move semantics, because it never hurts", but std::move
> can in fact be a pessimization:
>
> std::array<Foo, 10000> MakeHugeArray() {
> std::array<Foo, 10000> result;
> // ...
> return std::move(result);
> }

I suggest you talk to your compiler vendor (Hi!) and ask for a warning
for this case.

> As written, the return statement incurs 10,000 invocations of Foo's move
> constructor (or, worse, its copy constructor), but if std::move is
> eliminated, the return statement becomes effectively free, because it's an
> elidable copy/move. In this case it's not at all clear to me how to fix the
> problem; it seems like a fix would either require special-casing std::move
> (which seems like a hack, and breaks the convention that the core-language
> standard tries not to refer to the library standard), or require the
> implementation to 'see into' the implementation of a function called in a
> return statement (which seems impractical).
>
> I think it's worth trying to fix these issues, despite the difficulties,
> because being able to provide reliable "rules of thumb" would substantially
> improve the learning curve for std::move. Can anyone suggest how these
> issues could be fixed?

Per the above, I think we can make "you can leave out std::move when
returning a named local variable" be the right advice, with the added
bonus of a compiler warning if you put in a pessimizing std::move.

Xeo

unread,
Jun 24, 2013, 4:08:29 PM6/24/13
to std-pr...@isocpp.org
I remember sending you and Mike a reminder about that some months ago, and was wondering, did you actually talk about this at the Bristol meeting?

Richard Smith

unread,
Jun 24, 2013, 5:30:48 PM6/24/13
to std-pr...@isocpp.org
On Mon, Jun 24, 2013 at 1:08 PM, Xeo <hivem...@hotmail.de> wrote:
> I remember sending you and Mike a reminder about that some months ago, and
> was wondering, did you actually talk about this at the Bristol meeting?

No. For CWG, the Bristol meeting was almost entirely occupied by
reviewing C++14 feature wording. I don't recall seeing this on the
issues list, either, but perhaps I just missed it.
> --
>
> ---
> You received this message because you are subscribed to the Google Groups
> "ISO C++ Standard - Future Proposals" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to std-proposal...@isocpp.org.
> To post to this group, send email to std-pr...@isocpp.org.
> Visit this group at
> http://groups.google.com/a/isocpp.org/group/std-proposals/.
>
>

Mikhail Semenov

unread,
Jun 25, 2013, 1:55:47 PM6/25/13
to std-pr...@isocpp.org
I would suggest writing something like that:
 
std::unique_ptr<const Foo> FooFactory() {
  std::unique_ptr<const Foo> result(static_cast<const Foo*>(new Foo));
  // ...
  return result;
}
 
Then you don't need to deal with implicit conversions and writing return std::move(result);.
 
On the other hand, if you need to change the newly created Foo value: just deal with it first and then create the unique_ptr and return it.

Mikhail Semenov

unread,
Jun 25, 2013, 2:01:28 PM6/25/13
to std-pr...@isocpp.org
You don't actually need the static_cast here. The following will do:

std::unique_ptr<const Foo> FooFactory() {
  std::unique_ptr<const Foo> result(new Foo);
  // ...
  return result;
}
 

Ville Voutilainen

unread,
Jun 25, 2013, 2:14:37 PM6/25/13
to std-pr...@isocpp.org
Except that it won't for cases where the factory wants to modify the Foo stored inside the unique_ptr
before ultimately returning it as a unique_ptr to const Foo.

Mikhail Semenov

unread,
Jun 25, 2013, 2:46:25 PM6/25/13
to std-pr...@isocpp.org
If you really need to convert, I suggest make it explicit:
 
std::unique_ptr<const Foo> FooFactory() {
  std::unique_ptr<Foo> result(new Foo);
  // ...
  return std::unique_ptr<const Foo>(std::move(result));
}
 
It would be clear what you are doing.
 
By the way, where I work we always write explicit casts.

 

Jonathan Wakely

unread,
Jun 26, 2013, 5:18:24 AM6/26/13
to std-pr...@isocpp.org


On Tuesday, June 25, 2013 7:46:25 PM UTC+1, Mikhail Semenov wrote:
If you really need to convert, I suggest make it explicit:
 
std::unique_ptr<const Foo> FooFactory() {
  std::unique_ptr<Foo> result(new Foo);

*ahem* std::make_unique   ;-)

  // ...
  return std::unique_ptr<const Foo>(std::move(result));
}
 
It would be clear what you are doing.

It would be clear anyway with Geoffrey's suggestion, returning a unique_ptr of one type and having it convert to the return type.

If I didn't want the return type to differ from the declared type I'd have used return type deduction anyway, right?
 
By the way, where I work we always write explicit casts.

That's your prerogative, but doesn't mean it is the only way or that everyone else should have to do it that way.

Coming back to the original topic, are you really suggesting the code above is the ideal form for teaching when to use std::move and when not to?  I think "return result;" is much simpler.

Mikhail Semenov

unread,
Jun 26, 2013, 7:10:58 AM6/26/13
to std-pr...@isocpp.org
> Coming back to the original topic, are you really suggesting the code above is the ideal form for teaching when to use std::move and when > not to?  I think "return result;" is much simpler.
 
If you have to convert from std::unique_ptr<Foo> to std::unique_ptr<const Foo>
the following code WON'T COMPILE;
return result;
 
Try it in your compiler!
 
My point is that if you have to convert  (from std::unique_ptr<Foo> to std::unique_ptr<const Foo>) write it explicitly:
 
return std::unique_ptr<const Foo>(std::move(result));
 
If you don't have to convert (you use the same unque_ptr types) than yes write as you said:
 
return result.

 

Ville Voutilainen

unread,
Jun 26, 2013, 7:23:41 AM6/26/13
to std-pr...@isocpp.org
On 26 June 2013 14:10, Mikhail Semenov <mikhailse...@gmail.com> wrote:
> Coming back to the original topic, are you really suggesting the code above is the ideal form for teaching when to use std::move and when > not to?  I think "return result;" is much simpler.
 
If you have to convert from std::unique_ptr<Foo> to std::unique_ptr<const Foo>
the following code WON'T COMPILE;
return result;
 
Try it in your compiler!

We know. There's no need to shout. The whole question is whether it should be valid, aka compile.
The conversion in this case is perfectly safe, so perhaps we should consider relaxing the rules.

Jonathan Wakely

unread,
Jun 26, 2013, 8:08:41 AM6/26/13
to std-pr...@isocpp.org

Yep, I know, I was referring to Geoffrey's suggestion to change that rule in the first post in this thread: "It would be clear anyway with Geoffrey's suggestion".   Apologies for trying to discuss the thread topic ;)


Jonathan Wakely

unread,
Jun 26, 2014, 5:54:34 AM6/26/14
to std-pr...@isocpp.org


On Monday, 24 June 2013 22:30:48 UTC+1, Richard Smith wrote:
On Mon, Jun 24, 2013 at 1:08 PM, Xeo <hivem...@hotmail.de> wrote:
> I remember sending you and Mike a reminder about that some months ago, and
> was wondering, did you actually talk about this at the Bristol meeting?

No. For CWG, the Bristol meeting was almost entirely occupied by
reviewing C++14 feature wording. I don't recall seeing this on the
issues list, either, but perhaps I just missed it.


For the record, this is http://open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1579 which was moved to DR in February at the Issaquah meeting.

David Krauss

unread,
Jun 26, 2014, 9:17:26 PM6/26/14
to std-pr...@isocpp.org
On 2013–06–25, at 2:42 AM, Geoffrey Romer <gro...@google.com> wrote:

It's surprisingly hard to come up with a good, teachable set of rules for when to use std::move. For example, it would be nice to be able to say "you can leave out std::move when returning a named local variable", but this is not always the case.

That doesn’t sound like something that should be taught. Students shouldn’t have the impression that they should avoid move, or omit it just because they can.

Conversely, it would be nice to be able to say "When in doubt, just use std::move if you want move semantics, because it never hurts", but std::move can in fact be a pessimization:

std::array<Foo, 10000> MakeHugeArray() {
  std::array<Foo, 10000> result;
  // ...
  return std::move(result);
}

As written, the return statement incurs 10,000 invocations of Foo's move constructor (or, worse, its copy constructor), but if std::move is eliminated, the return statement becomes effectively free, because it's an elidable copy/move.

I'm pretty sure there’s a DR for this. First-draft code doesn’t need to run full speed anyway. Students should learn general techniques for finding bottlenecks, not every performance quirk and pitfall. When fixing this code, it would be a good idea to add a comment that the result is expected to be constructed in-place, hence neither moved nor copied.

By the way, this code is still giving you exactly what you asked for. Asking for a move when you actually want copy elision is, though reasonable, a confusion of two completely different things.

In this case it's not at all clear to me how to fix the problem; it seems like a fix would either require special-casing std::move (which seems like a hack, and breaks the convention that the core-language standard tries not to refer to the library standard), or require the implementation to 'see into' the implementation of a function called in a return statement (which seems impractical). 

It may be doable for inline functions. Perhaps some future language iteration will see through the thicket.

I think it's worth trying to fix these issues, despite the difficulties, because being able to provide reliable "rules of thumb" would substantially improve the learning curve for std::move. Can anyone suggest how these issues could be fixed?

Say move() when you want a move. Debug the rest, and be aware that moves still carry a cost which may be as much as a copy.

Krzysztof Ostrowski

unread,
Jun 27, 2014, 2:22:11 PM6/27/14
to std-pr...@isocpp.org

I learnt that you should apply std::move only to the types that hold resources (like heap memory) behind the scenes. That means you shouldn't apply std::move to fundamental types and types that are bitwise copyable (with shallow copy operation like aggregates). In fact, this applies to std::array as well, but is limited to the implementation of type stored in such array. Typical implementation of std::array might be:


template<class T, size_t n>
array
{ T storage[n]; /* some non-virual member function here... */  }

What if T is not bitwise copyable or it is, for instance std::vector<U> of thousands of U? Should I apply move operation to every element of such std::array<std::vector<U>, n>?  It depends mainly on N/RVO optimisations. To be strict, moving blindly may not be a good idea.

Issuing a compiler warning on possible pessimization when applying std::move is reasonable. Implicit conversion are evil in my opinion and such conversion that changes properties of a type (like CV-qualifiers) should result in warning. Compiler warnings are to help developers.


K

Geoffrey Romer

unread,
Jun 27, 2014, 3:43:33 PM6/27/14
to std-pr...@isocpp.org
On Thu, Jun 26, 2014 at 6:17 PM, David Krauss <pot...@gmail.com> wrote:

On 2013–06–25, at 2:42 AM, Geoffrey Romer <gro...@google.com> wrote:

It's surprisingly hard to come up with a good, teachable set of rules for when to use std::move. For example, it would be nice to be able to say "you can leave out std::move when returning a named local variable", but this is not always the case.

That doesn’t sound like something that should be taught. Students shouldn’t have the impression that they should avoid move, or omit it just because they can.

Really? You wouldn't even teach students to omit move in an expression like this?

Sink(std::move(Source()));


Conversely, it would be nice to be able to say "When in doubt, just use std::move if you want move semantics, because it never hurts", but std::move can in fact be a pessimization:

std::array<Foo, 10000> MakeHugeArray() {
  std::array<Foo, 10000> result;
  // ...
  return std::move(result);
}

As written, the return statement incurs 10,000 invocations of Foo's move constructor (or, worse, its copy constructor), but if std::move is eliminated, the return statement becomes effectively free, because it's an elidable copy/move.

I'm pretty sure there’s a DR for this. First-draft code doesn’t need to run full speed anyway. Students should learn general techniques for finding bottlenecks, not every performance quirk and pitfall.

The best way to avoid teaching performance quirks and pitfalls is to eliminate them from the language. Failing that, students will have to learn them eventually, although you can haggle over when.
 
When fixing this code, it would be a good idea to add a comment that the result is expected to be constructed in-place, hence neither moved nor copied.

By the way, this code is still giving you exactly what you asked for. Asking for a move when you actually want copy elision is, though reasonable, a confusion of two completely different things.

Can you give an example of when a programmer would not want a copy elision?
 

In this case it's not at all clear to me how to fix the problem; it seems like a fix would either require special-casing std::move (which seems like a hack, and breaks the convention that the core-language standard tries not to refer to the library standard), or require the implementation to 'see into' the implementation of a function called in a return statement (which seems impractical). 

It may be doable for inline functions. Perhaps some future language iteration will see through the thicket.

I think it's worth trying to fix these issues, despite the difficulties, because being able to provide reliable "rules of thumb" would substantially improve the learning curve for std::move. Can anyone suggest how these issues could be fixed?

Say move() when you want a move.

That's just begging the question; I'm trying to teach people how to know when they should want a move
 
Debug the rest, and be aware that moves still carry a cost which may be as much as a copy. 

Would you say, then, that one should never apply std::move to a copyable type?

David Krauss

unread,
Jun 27, 2014, 6:30:10 PM6/27/14
to std-pr...@isocpp.org
On 2014–06–28, at 3:43 AM, 'Geoffrey Romer' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:


On Thu, Jun 26, 2014 at 6:17 PM, David Krauss <pot...@gmail.com> wrote:

That doesn’t sound like something that should be taught. Students shouldn’t have the impression that they should avoid move, or omit it just because they can.

Really? You wouldn't even teach students to omit move in an expression like this?

Sink(std::move(Source()));

That’s going a bit far. Students should know that move casts an object-semantic lvalue into a value-semantic rvalue, and application to an rvalue is always redundant. But that’s not avoiding it, or opportunistic omission, it follows from the underlying theory.

When fixing this code, it would be a good idea to add a comment that the result is expected to be constructed in-place, hence neither moved nor copied.

By the way, this code is still giving you exactly what you asked for. Asking for a move when you actually want copy elision is, though reasonable, a confusion of two completely different things.

Can you give an example of when a programmer would not want a copy elision?

It’s an optimization, not a choice. Teaching its interaction with moving is asks the student to think about two theories at once, which almost by definition relegates it to a more advanced course. 

Say move() when you want a move.

That's just begging the question; I'm trying to teach people how to know when they should want a move

I mean, say move() when you want an ownership transfer.

Debug the rest, and be aware that moves still carry a cost which may be as much as a copy. 

Would you say, then, that one should never apply std::move to a copyable type?

Absolutely not. For one thing, that often happens in generic programming. I simply mean that while chasing performance bugs, after the fact of writing a first draft, code with move semantics should not be dismissed out of hand as “already fast.”

Geoffrey Romer

unread,
Jun 27, 2014, 7:22:17 PM6/27/14
to std-pr...@isocpp.org
On Fri, Jun 27, 2014 at 3:30 PM, David Krauss <pot...@gmail.com> wrote:

On 2014–06–28, at 3:43 AM, 'Geoffrey Romer' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:


On Thu, Jun 26, 2014 at 6:17 PM, David Krauss <pot...@gmail.com> wrote:

That doesn’t sound like something that should be taught. Students shouldn’t have the impression that they should avoid move, or omit it just because they can.

Really? You wouldn't even teach students to omit move in an expression like this?

Sink(std::move(Source()));

That’s going a bit far. Students should know that move casts an object-semantic lvalue into a value-semantic rvalue, and application to an rvalue is always redundant. But that’s not avoiding it, or opportunistic omission, it follows from the underlying theory.

I'm curious; if you don't think students should be taught about this option, do you think it was a mistake for the language to go so far out of its way (q.v. [class.copy]/p32) to enable it?

In any event, this is a moot point. C++14 resolves the defect I was referring to here, so we _can_ now teach that you can always omit std::move when returning a variable that's local to the scope you're returning from.
 

When fixing this code, it would be a good idea to add a comment that the result is expected to be constructed in-place, hence neither moved nor copied.

By the way, this code is still giving you exactly what you asked for. Asking for a move when you actually want copy elision is, though reasonable, a confusion of two completely different things.

Can you give an example of when a programmer would not want a copy elision?

It’s an optimization, not a choice. Teaching its interaction with moving is asks the student to think about two theories at once, which almost by definition relegates it to a more advanced course. 

Yes, which is exactly I don't want it to have _any_ interaction with moving, so that I don't have to teach about that interaction.


Say move() when you want a move.

That's just begging the question; I'm trying to teach people how to know when they should want a move

I mean, say move() when you want an ownership transfer.

"Ownership" normally refers to the responsibility to explicitly invoke a cleanup operation (cf. [unique.ptr]/p1), and so doesn't apply to many situations where move() is appropriate, or even necessary. It's possible to generalize the notion of ownership so that it applies to those situations too, but at that point "ownership transfer" becomes basically synonymous with move, and your advice again becomes circular.

This rule also doesn't account for the Sink(Source()) example above.
 

Debug the rest, and be aware that moves still carry a cost which may be as much as a copy. 

Would you say, then, that one should never apply std::move to a copyable type?

Absolutely not. For one thing, that often happens in generic programming. I simply mean that while chasing performance bugs, after the fact of writing a first draft, code with move semantics should not be dismissed out of hand as “already fast.”

Oh, I certainly agree with that; my goal is to reduce the number of performance bugs that people write (or fear they will write) in the first place.

David Krauss

unread,
Jun 28, 2014, 12:10:47 PM6/28/14
to std-pr...@isocpp.org
On 2014–06–28, at 7:22 AM, 'Geoffrey Romer' via ISO C++ Standard - Future Proposals <std-pr...@isocpp.org> wrote:

On Fri, Jun 27, 2014 at 3:30 PM, David Krauss <pot...@gmail.com> wrote:

That’s going a bit far. Students should know that move casts an object-semantic lvalue into a value-semantic rvalue, and application to an rvalue is always redundant. But that’s not avoiding it, or opportunistic omission, it follows from the underlying theory.

I'm curious; if you don't think students should be taught about this option, do you think it was a mistake for the language to go so far out of its way (q.v. [class.copy]/p32) to enable it?

[class.copy]/32 specifies that copy elision “failure” shall decay to a move. That’s a way of obtaining a lesser opportunistic optimization, as gradual degradation.

I didn’t say students shouldn’t be taught about anything. I merely recommend an order of curriculum:

1. Rvalues and lvalues (why and how you are prevented from assigning 1 = 3). Value semantics vs object semantics: refer to functional and imperative programming paradigms.
2. move() is used to constrain an object to temporarily behave more like a pure value.
— Perhaps some time later in the course —
3. Copy elision: object semantics may be ignored if they get in the way. The compiler hacks a chain of transitively initialized objects to be identically the same object.
4. The compiler is allowed to use move semantics as an inferior substitute for copy elision. (In fact, it must use this fallback.)

Students should never be taught to use an opportunistic optimization as if it were a means of expression. It’s slightly unfortunate that there is no explicit way to express copy elision. If there’s a problem with the language, it’s the lack of an explicit require_copy_elide operator. (I don’t take that seriously.)

In any event, this is a moot point. C++14 resolves the defect I was referring to here, so we _can_ now teach that you can always omit std::move when returning a variable that's local to the scope you're returning from.

What you do in your classroom is your business. I would prefer to work with people who don’t try to omit anything that they mean. (Of course, meaning should be kept to a zen minimum.) Please, encourage students to write comments.

It’s an optimization, not a choice. Teaching its interaction with moving is asks the student to think about two theories at once, which almost by definition relegates it to a more advanced course. 

Yes, which is exactly I don't want it to have _any_ interaction with moving, so that I don't have to teach about that interaction.

The easiest thing to do is to not teach about optimizations. They are nice, but complicated, things the compiler does for you. Copy elision can be put in the same bucket as loop unrolling, function inlining, loop-invariant code motion.

Copy elision happens not to be covered by the as-if rule, hence its mention in the standard. But unexpected failure of a constructor to run is a quirk, not a feature. In that sense, you only need to tell students that copy/move constructors should not have side effects because they may be skipped, but there’s no need to explain when or why.

"Ownership" normally refers to the responsibility to explicitly invoke a cleanup operation (cf. [unique.ptr]/p1), and so doesn't apply to many situations where move() is appropriate, or even necessary. It's possible to generalize the notion of ownership so that it applies to those situations too, but at that point "ownership transfer" becomes basically synonymous with move, and your advice again becomes circular.

Perhaps my meaning hasn’t come across. You might ask (or find an existing answer) on StackOverflow. Maybe the object vs value semantic idea is better. The important thing is to settle on something you’re comfortable explaining to the class. But, I assure you that there's clear distinction, and it’s not only a matter of getting the desired constructor to be called.

Oh, I certainly agree with that; my goal is to reduce the number of performance bugs that people write (or fear they will write) in the first place.

That’s a dangerous path. Performance bugs aren’t avoided. Knowledge of algorithms and structures is sufficient to produce a program which, once ironed out using a profiler, will be optimal. Students need to know how to operate a profiler and how to interpret a program’s interaction with the machine architecture. They should *not* be encouraged to get hung up on language quirks.

The physical machine is what goes fast, not the C++ virtual machine!

Reply all
Reply to author
Forward
0 new messages