4

For example:

Big create()
{
    Big x;
    return std::move(x);
//  return static_cast<typename std::remove_reference<T>::type&&>(t) // why not elide here?
}

Assuming that applying std::move() to return a local variable inhibits move-semantics because compilers can't make any assumptions about the inner-workings of functions in general, what about cases when those assumptions are not necessary, for example when:

  1. std::move(x) is inlined (probably always)
  2. std::move(x) is written as: static_cast<typename std::remove_reference<T>::type&&>(t)

According to the current Standard, an implementation is allowed to apply NRVO...

— 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 (18.3)) 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 call’s return object

Obviously, neither 1) nor 2) qualify. Apart from the fact that using std::move() to return a local variable is redundant, why is this restriction necessary?

Leo Heinsaar
  • 3,887
  • 3
  • 15
  • 35
  • 3
    Casts like std::move() are performed at compile time, so elision is moot; they are simply instructions to the compiler to change how sees a type. –  Aug 07 '17 at 18:50
  • 3
    [Using `std::move()` on a returned object is redundant.](https://stackoverflow.com/a/14856553/501250) – cdhowie Aug 07 '17 at 18:51
  • 1
    "Assuming `std::move()` on a local variable inhibits move semantics because etc. etc." - Doesn't sound right. – einpoklum Aug 07 '17 at 18:57
  • 1
    It seems that you misunderstand the question. The question is: why does the standard allow NRVO **only** in the case of "when the expression is the name of a non-volatile automatic object". – geza Aug 07 '17 at 19:12
  • @geza, question could be presented better. I hope I answered it, though. – SergeyA Aug 07 '17 at 21:28
  • 1
    @SergeyA: I think the OP asks the rationale behind this restriction, why it is there. And to be honest, I don't understand it either. If the compiler is able to prove that an expression refers to a local variable, it could apply elision. I don't understand why it is allowed only when it is strictly a name. I've asked a related question: https://stackoverflow.com/questions/45561234/is-copy-move-elision-allowed-when-returning-object – geza Aug 08 '17 at 06:59

2 Answers2

2

After re-reading the question, I understand it differently. I read the question as 'Why std::move() inhibits (N)RVO'

Quote from standard provided in the question has wrong highlight. It should be

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 (18.3)) with the same type (ignoring cv-qualification) as the function return type

What inhibits NRVO here is not that std::move() is called, but the fact that return value of std::move is not X, but X&&. It doesn't match the function signature!

SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • Although this is good advice, I don't think it actually answers the question. – Nir Friedman Aug 07 '17 at 19:47
  • @NirFriedman, well I believe it does, by mooting (is it a word?) the whole question. – SergeyA Aug 07 '17 at 20:25
  • 1
    I mean, because the answer to a question is not necessary to write the best C++, does not mean the question is invalid. "Don't do X" is not an answer to "Why doesn't X work this way", especially when it's extremely clear that the author is already aware. As it stands, most of the information in this answer is already in the question. – Nir Friedman Aug 07 '17 at 21:00
  • @NirFriedman, as a matter of fact, after carefully re-reading the question, I believe, you are right. I think, the question could be much cleaner, though. – SergeyA Aug 07 '17 at 21:11
  • In fairness, you may have responded to the first version of the question, the edits have improved it quite a bit IMHO. – Nir Friedman Aug 07 '17 at 21:13
  • 1
    Actually, the expression does have the same type, but a different value category. The only true problem is that it's not a name. – aschepler Aug 07 '17 at 23:29
  • @aschepler, I disagree. Completely. `return Foo()` or `return make_foo()` would all be elided alright for the function returning `Foo`. – SergeyA Aug 08 '17 at 13:17
  • @aschepler Can you elaborate? I'm not sure I understand your point; SergeyA's counter-examples are not named but would still get NRVO'ed. Also, the types are different. `T` and `T&&` are different types. – Nir Friedman Aug 08 '17 at 14:11
  • @SergeyA I'm honestly not sure, my reading of the question is that they want to know *why* an exact match is required of the types. Why couldn't it be more permissive and allow NRVO even when the types are `X&&` vs `X`? – Nir Friedman Aug 08 '17 at 14:14
  • 1
    @NirFriedman, OP keeps silent, so I do not think it is worth going any further here. Off the record, I think that standard (for obvious reasons) didn't want to allow conversion constructors to be elided, and didn't worth it's while to craft an exception for decays. – SergeyA Aug 08 '17 at 14:39
  • @SergeyA In C++14, those are covered by a different bullet: "when a temporary class object that has not been bound to a reference would be copied/moved to a class object with the same cv-unqualified type". In C++17, that bullet will be removed because those cases do not have any move constructor call that might be elided in the first place. – aschepler Aug 08 '17 at 23:00
  • @NirFriedman An expression never has a reference type. `std::move(x)` in the OP is an expression of type `Big`, and is classified as an xvalue. – aschepler Aug 08 '17 at 23:02
2

You should be clear on exactly what "allow elision" means. First of all, the compiler can do anything it wants, under the "as-if" rule. That is, the compiler can spit out any assembly it wants, as long as that assembly behaves correctly. That means that the compiler can elide any constructor it wants, but it does have to prove that the program will behave the same whether or not the constructor is called.

So why the special rules for elision? Well, these are cases where the compiler can elide constructor calls (and therefore, destructor calls too) without proving that the behavior is the same. This is very useful, because there are lots of types where the constructor is very non-trivial (like say, string), and the compilers in practice are generally not capable of proving that they are safe to elide (in a reasonable time frame) (in the past, there was even lack of clarity on whether optimizing out a heap allocation was legal to begin with, since it is basically mutation of a global variable).

So, we want to have elision for performance reasons. However, it is basically designating a special case in the standard, in terms of behavior. The bigger the special case, the more complexity we are introducing to the standard. So the goal should be to make the permitted situation for elision to be broad enough to cover the useful cases we care about, but no broader.

You are approaching this as: why not make the special case as big as practical? In reality, it is the opposite. To extend the allowable situations for elision, it needs to be shown to be very worthwhile.

Nir Friedman
  • 17,108
  • 2
  • 44
  • 72