Compiler deduction of rvalue-references for variables going out of scope
Asked Answered
S

4

9

Why won't the compiler automatically deduce that a variable is about to go out of scope, and therefore let it be considered an rvalue-reference?

Take for example this code:

#include <string>

int foo(std::string && bob);
int foo(const std::string & bob);

int main()
{
    std::string bob("  ");
    return foo(bob);
}

Inspecting the assembly code clearly shows that the const & version of "foo" is called at the end of the function.

Compiler Explorer link here: https://godbolt.org/g/mVi9y6

Edit: To clarify, I'm not looking for suggestions for alternative ways to move the variable. Nor am I trying to understand why the compiler chooses the const& version of foo. Those are things that I understand fine.

I'm interested in knowing of a counter example where the compiler converting the last usage of a variable before it goes out of scope into an rvalue-reference would introduce a serious bug into the resulting code. I'm unable to think of code that breaks if a compiler implements this "optimization".

If there's no code that breaks when the compiler automatically makes the last usage of a variable about to go out of scope an rvalue-reference, then why wouldn't compilers implement that as an optimization?

My assumption is that there is some code that would break where compilers to implement that "optimization", and I'd like to know what that code looks like.

The code that I detail above is an example of code that I believe would benefit from an optimization like this.

The order of evaluation for function arguments, such as operator+(foo(bob), foo(bob)) is implementation defined. As such, code such as

return foo(bob) + foo(std::move(bob));

is dangerous, because the compiler that you're using may evaluate the right hand side of the + operator first. That would result in the string bob potentially being moved from, and leaving it in a valid, but indeterminate state. Subsequently, foo(bob) would be called with the resulting, modified string.

On another implementation, the non-move version might be evaluated first, and the code would behave the way a non-expert would expect.

If we make the assumption that some future version of the c++ standard implements an optimization that allows for the compiler to treat the last usage of a variable as an rvalue reference, then

return foo(bob) + foo(bob);

would work with no surprises (assuming appropriate implementations of foo, anyway).

Such a compiler, no matter what order of evaluation it uses for function arguments, would always evaluate the second (and thus last) usage of bob in this context as an rvalue-reference, whether that was the left hand side, or right hand side of the operator+.

Sidestroke answered 18/8, 2017 at 19:38 Comment(16)
You explicitely told the compiler not to do the move optimization by declaring foo(const string&). What else do you expect?Jedthus
What? Where did I tell the compiler not to optimize the code I provided as an example?Sidestroke
By declaring foo(const string&). It is not about optimization,. The compiler does not guess what to optimize. It calls the best fitting function to do the job. You provided one. it calls it. Your own decision.Jedthus
You seem to be entirely missing the point of my question? I'm well aware that the const& version of the function would be called in my example. I'm asking why that's the case. My stance is that the const& version is not the best fitting, as the compiler knows that bob is about to go out of scope. I'm trying to find example code where the compiler replacing foo(bob) with foo(std::move(bob)) would generate code that violated some aspect of the language so that I can better understand the language.Sidestroke
It is the case because YOU defined a foo(const string&). That is the only reason the compiler chose that function. Don't try to figure out another reason, there is none,Jedthus
Still missing the point of my question. I'm perfectly aware of the way the compiler resolves the correct function to call. I'm not asking for information on that. I'm asking for counter examples where the "optimization" i'm postulating would cause currently valid code to work improperly. Please re-read the question.Sidestroke
The code you are seeking you already wrote. In the case of 2 functions, one taking the parameter as a const ref, the other taking the parameter as an rvalue-move-ref, the compiler will always chose to pass a variable by const ref because the semantics of the two functions may differ, and picking one at random would surely lead to undefined behaviour.Jedthus
Still.... missing the point of my question. My goodness. I understand all of that already, I've been using c++ since the early 2000's. I'm not asking about picking one at random, i'm not asking about the semantics of two different functions with the same name. I'm asking about whether an optimization such as what I'm asking about in the original question would break existing code, and if so, which code would break?Sidestroke
You did not read my previous comment, obviously. There would be undefined behaviour if there is any small diference in semantics in the two distinct functions foo(const string&) and foo(string&&), As a reminder these functions are so distinct, they each have their own body. It is not for the compiler to guess what they do, because YOU, the programmer provided these two functions explicitly, and the compiler must do what YOU asked for.Jedthus
No, I did read your comment. Please stop implying that I don't understand these things that you're talking about. They are irrelevant to my question, as I've said before.Sidestroke
Well you should have your answer, then. All three answers below have also stated the same.Jedthus
They all state "the compiler isn't allowed to do that", which I already know, and isn't what I'm asking. I'm asking "What would break if a compiler implemented some kind of optimization like what I'm describing". Nir Friedman provides the most comprehensive attempt to get to the bottom of things, but I'm not satisfied with the last of concrete resolution (not Nir's fault, obviously).Sidestroke
What would break is that the wrong function would get called. That in itself is crazy enough that you can be assure we will never see the optimization you talk about. It will never happen..For this particular reason.Jedthus
You say "wrong function", I say "correct function". The difference between foo(std::string const&) and foo(std::string &&) wasn't significant enough for the standards committee to reject foo(std::string()) from calling foo(std::string&&), I'm skeptical that it's a relevant difference for what I'm asking about in this SO question.Sidestroke
@Sidestroke this was an interesting question and I'm glad you asked it. I feel bad for the trouble you had to go through to get people to understand your question. Michaël Roy clearly didn't get it, and those that simply said "the standard doesn't allow it" were missing the point. Even the accepted answer is lacking, because run_some_async_calculation_on_vector would have to make a copy of the vector anyway, since the async calculation presumably continues after it returns (at which point its arguments, namely v would be deallocated anyway).Whisky
@ricovox Thanks! I also wasn't completely satisfied with run_some_async_calculation_on_vector, as I thought the polymorphic allocators having such strange behavior on move was a very weird choice for the language, but technically, it did provide me a counter example, so that's what I was looking for :-)Sidestroke
G
5

Here's a piece of perfectly valid existing code that would be broken by your change:

// launch a thread that does the calculation, moving v to the thread, and
// returns a future for the result
std::future<Foo> run_some_async_calculation_on_vector(std::pmr::vector<int> v); 

std::future<Foo> run_some_async_calculation() {
    char buffer[2000];
    std::pmr::monotonic_buffer_resource rsrc(buffer, 2000);
    std::pmr::vector<int> vec(&rsrc);
    // fill vec
    return run_some_async_calculation_on_vector(vec);
}

Move constructing a container always propagates its allocator, but copy constructing one doesn't have to, and polymorphic_allocator is an allocator that doesn't propagate on container copy construction. Instead, it always reverts to the default memory resource.

This code is safe with copying because run_some_async_calculation_on_vector receives a copy allocated from the default memory resource (which hopefully persists throughout the thread's lifetime), but is completely broken by a move, because then it would have kept rsrc as the memory resource, which will disappear once run_some_async_calculation returns.

Gonzales answered 18/8, 2017 at 20:53 Comment(3)
Marked as answer because it succinctly demonstrates code that would break in a subtle way where the compiler to deduce rvalue-reference from a variable that was about to go out of scope.Sidestroke
Once run_async_calculation returns, its argument v would be out of scope anyway. So for some async operation to access it, it must be stored in a location that will persist during the async operation. So either (A) a copy of the vector is made (in the body of run_async_calculation) and so the OP's suggested optimization would NOT break it or (B) the vector is moved inside run_async_code causing it to encounter the same problem you describe--if v has a temporary memory resource, it will not be available during the async operation. Thus the code is broken to begin with.Whisky
@ricovox Nope. The contract of run_some_async_calculation_on_vector is "give me a vector that I can move to the thread". If you break the contract and give it a vector using a temporary memory resource, that's your own fault.Gonzales
P
1

The answer to your question is because the standard says it's not allowed to. The compiler can only do that optimization under the as if rule. String has a large constructor and so the compiler isn't going to do the verification it would need to.

To build on this point a bit: all that it takes to write code that "breaks" under this optimization is to have the two different versions of foo print different things. That's it. The compiler produces a program that prints something different than the standard says that it should. That's a compiler bug. Note that RVO does not fall under this category because it is specifically addressed by the standard.

It might make more sense to ask why the standard doesn't say so, e.g.why not extend the rule governing returning at the end of a function, which is implicitly treated as an rvalue. The answer is most likely because it rapidly becomes complicated to define correct behavior. What do you do if the last line were return foo(bob) + foo(bob)? And so on.

Pathe answered 18/8, 2017 at 19:49 Comment(10)
Could you clarify on where the standard says it's not allowed? When you say it can only do that optimization under the as-if rule, are you saying it can only do any optimization under that rule, or only optimizations of this nature? RVO and NRVO in previous iterations of the language were done by compilers even though they clearly had behavior differences due to eliding of constructors / destructors.Sidestroke
Your second paragraph is more interesting, as it raises questions of what is the appropriate logic to use. In your example of "return foo(bob) + foo(bob);" I had assumed that because the compiler knows the evaluation order it's using for, e.g. function call arguments and so on, the compiler would simply have the last such call by the rvalue-ref version, whichever that call happened to be for the way the compiler orders things.Sidestroke
in foo(bob) + foo(bob) the compiler will only call the const ref version of foo(). That is the one that fits the parameters best. The compiler is very deterministic and rigid on that point. It has to be.Jedthus
Still missing the point of my question, @MichaëlRoy. I'd like to ask you to please re-read it and provide feedback on what I'm actually asking about. To attempt to clarify. I don't care what the compiler does now, I care about why the compiler isn't taking advantage of a potential optimization. I suspect that the optimization I'm referring to in my question would actually break real-world code, and am trying to find an example of code that would break where the compiler to automatically treat the last usage of a variable as an rvalue-reference.Sidestroke
@Sidestroke I mean, the standard defines what an lvalue is and what an rvalue is, and that is an lvalue. And the standard specifies that since bob is an lvalue, it must bind to the second foo overload. That's all there is to it. Now, the compiler can do any optimization it wants under the as-if rule, but as-if is obviously a pretty strict guideline. RVO and NRVO are not under the as-if rule, and in fact RVO is specifically addressed by the standard. So the compiler can do RVO, because the standard says so.Pathe
@NirFriedman Ok. So lets assume there existed a draft specification of c++2x that allowed the compilers to implement an optimization like I ask about in my question. That would allow compilers to implement the theoretical optimization without following the as-if rule. What code breaks? I suspect there is code that would break. I'm trying to understand what such code would look like.Sidestroke
@Sidestroke As for the second: well even the order of function evaluation arguments is not defined: operator+(foo(bob), foo(bob)) it's not defined which foo even gets called first. My broader point here is that this is a leaky ship. I can point to a whole, and you can plug it, but there will be another hole, and another hole. Is there really a magical simple rule that works well, isn't too shocking, doesn't leave a crazy corner case with some other rule? Maybe, but I doubt it. But if you think you have it, you could propose it to the committee.Pathe
@Sidestroke That is basically my whole point, it is not clear what exactly that draft specification would look like. You are thinking like a user, considering the most useful and common case. Think like a language writer or compiler implementer: you need to cover all cases, including very pathological things. RVO, and implicit treatment of return as rvalue are much easier to specify than what you are suggesting. In the common case they appear similar but in generality they are not.Pathe
@NirFriedman I honestly don't see how RVO and implicit treatment of return as rvalue are much easier to specify, though clearly I'm not explaining myself well enough for people trying to answer my question to understand what I mean. Perhaps I will propose it to the committee, though like I said before, I don't know that the concept I'm asking about is actually legitimate, and am leaning towards an optimization like this breaking something, I just can't think of any counter examples despite trying to shrug.Sidestroke
Let us continue this discussion in chat.Pathe
C
0

Because the fact it will go out of scope does not make it not-an-lvalue while it is in scope. So the - very reasonable - assumption is that the programmer wants the second version of foo() for it. And the standard mandates this behavior AFAIK.

So just write:

int main()
{
    std::string bob("  ");
    return foo(std::move(bob));
}

... however, it's possible that the compiler will be able to optimize the code further if it can inline foo(), to get about the same effect as your rvalue-reference version. Maybe.

Campanulate answered 18/8, 2017 at 19:43 Comment(0)
B
0

Why won't the compiler automatically deduce that a variable is about to go out of scope, and can therefore be considered an rvalue-reference?

At the time the function gets called, the variable is still in scope. If the compiler changes the logic of which function is a better fit based on what happens after the function call, it will be violating the standard.

Bili answered 18/8, 2017 at 19:43 Comment(5)
Why would that be violating the standard?Sidestroke
@jonesmz, the standard defines the behavior of the program based on everything prior to a statement, not after a statement.Bili
@jonesmz: Are you asking "why the standard is the way it is"?Campanulate
No, I'm asking where in the standard is an optimization of this nature disallowed, or alternatively looking for an example of code that would break on a compiler that deduces an rvalue-reference on a value going out of scope.Sidestroke
@jonesmz, I'll need some time to look that up.Bili

© 2022 - 2024 — McMap. All rights reserved.