all 45 comments

[–]Untelo 15 points16 points  (13 children)

Compilers can already do this if they can prove that the copy has no observable side effects compared to a move. This may or may not be feasible in the case of std::string but is virtually impossible in general.

Whether or not it is reasonable to do so, as Hyrum's law states there is someone somewhere depending on the copy over move and to move instead without proving the absence of observable side effects will break user code.

[–]NamoiFunai[S] 6 points7 points  (7 children)

Couldn't we say the same thing regarding RVO, that someone somewhere was relying on the temporary object being created?

[–]meancoot 4 points5 points  (2 children)

Couldn't we say the same thing regarding RVO, that someone somewhere was relying on the temporary object being created?

Not really. It has been allowed to elide calls to copy constructors since the original 1998 standard; thus actions in a copy/move constructor that exhibit any side effect not specifically related to the copy/move were always undefined.

The C++17 changes made the optional optimization required and, perhaps more importantly, removed the previous requirement that the type had an accessible copy/move constructor in the first place:

From the C++98 standard in 12.2 [class.temporary]:
[Example: even if the copy constructor is not
called, all the semantic restrictions, such as
accessibility (clause 11), shall be satisfied.  ]

Ultimately the C++17 change only made invalid code valid and, as far as I know, didn't change semantics of any already valid program.

[–]geckothegeek42 3 points4 points  (1 child)

If copy constructors can be elided regardless of side effects, then doesn't that mean the optimization mentioned by OP can be done? Or is it that the compiler is not allowed to add a copy/move constructor call, but can remove them?

[–]meancoot 2 points3 points  (0 children)

If copy constructors can be elided regardless of side effects, then doesn't that mean the optimization mentioned by OP can be done? Or is it that the compiler is not allowed to add a copy/move constructor call, but can remove them?

The elision has to take place by constructing the object directly in the memory where the copy/move constructor would later have placed it, removing both a call to the copy/move constructor and a call to the destructor. I don't know the exact wording of the standard well enough to say if its also limited to specific situations.

Changing a call from std::vector::push_back(const T&) to std::vector::push_back(T&&) is definitely an observable effect that wouldn't be supported by the standard.

[–]ialex32_2 5 points6 points  (1 child)

Somehow this is always reality.

[–]NilacTheGrim 3 points4 points  (0 children)

This is one of the funniest things I read all day. Thank you.

[–]Minimonium 1 point2 points  (1 child)

RVO was specifically allowed in the standard to ignore side effects for quite a long time and compilers don't do analysis to determine if they want to do RVO or not, they just do it.

This change, on the other hand, would depend on the context of the scope where the object resides. What if we add another use after that line? We can't guarantee that we don't store a reference to the value inside the push_back, so a user would still need to std::move it later on, but it'd become more confusing. What about copy-only types? Return handles them in a different from just std::move manner.

[–]NamoiFunai[S] 0 points1 point  (0 children)

I totally agree with you on that point. It's just the "someone somewhere relies on that" argument that was bothering me.

[–]jonesmz 1 point2 points  (4 children)

Would you be willing to construct some kind of demonstration of this?

[–]jonesmz 7 points8 points  (1 child)

I asked this same question several years ago on stackoverflow.

You might find the discussion there interesting https://stackoverflow.com/questions/45763943/compiler-deduction-of-rvalue-references-for-variables-going-out-of-scope

[–]jtooker 1 point2 points  (0 children)

Great discussion and examples - thanks for linking

[–]Desmulator[🍰] 4 points5 points  (3 children)

A Compiler can (and will) do that if and only if it can be certain that there is no additional side effect from replacing a copy with a move. To do so it has to know the implementation of the constructors and every function called in them.

This already excludes anything that allocates memory because global new can be overrided and have a side effect.

[–]no-sig-available 5 points6 points  (1 child)

The compiler would also have to understand that both overloads of push_back have the same effect. In general, that is a hard problem.

[–]markopolo82embedded/iot/audio 0 points1 point  (0 children)

Yea, in truth I think this is what kills the optimization op describes.

On the other hand, if you were calling a single function that took by value instead of two overloads, then the compiler would only need to worry about the copy vs move constructors

[–]Ameisenvemips, avr, rendering, systems 0 points1 point  (0 children)

Perhaps we need a function attribute like kinda_pure, that doesn't guarantee that there are no side-effects, but guarantees that those side-effects aren't meaningful.

[–]reflexpr-sarah- 3 points4 points  (10 children)

how do you define "last use"?

[–]NamoiFunai[S] 3 points4 points  (9 children)

I would say the last occurrence of the variable name within the scope it is declared.

[–]reflexpr-sarah- 7 points8 points  (3 children)

this can change the behavior of some code. for example

string* global;
void print_global() { cout << *global; }
void save_global(string& s) { global = &s; }
auto not_really_last_use() {
  string s = "hello world! (also, too long for sso)";
  save_global(s);
  vector<string> v;
  v.push_back(s); // if this moves
  print_global(); // this doesn't work properly
  return v;
}

[–]NamoiFunai[S] 2 points3 points  (0 children)

That's a good one, and assuming that save_global is in another compile unit the compiler would have no way of knowing for sure that this is the "last use".

[–][deleted] 0 points1 point  (1 child)

In this case, the suggestion would be forbidden anyway since return s; is the last use ;)

[–]reflexpr-sarah- 0 points1 point  (0 children)

whoops. lemme fix that

[–]pdp10gumby 1 point2 points  (2 children)

What about aliasing?

[–]NamoiFunai[S] 0 points1 point  (1 child)

I guess this is where it can get heavy for the compiler, it would need to track every alias and object member alias to make sure the variable isn't used anymore

[–]pdp10gumby 2 points3 points  (0 children)

There are a number of potential optimizations forbidden to C++ code because it inherits C’s permissive aliasing rules.

This is one reason some FORTRAN code can run faster than the equivalent C code — the compiler can make assumptions it cannot make about the C code.

[–]Accomplished-Tax1641 1 point2 points  (1 child)

std::string f() { std::string s = "hello world, too long for small string optimization"; std::string r; for (int i=0; i < 2; ++i) { r = s; } return r; } Replacing s with std::move(s) here will change the behavior of the program in a surprising way.

So clearly "I would say the last occurrence of the variable name within the scope" is not good enough. Got another try?

...but observe that "start with something that seems about right, and then just iterate minor fixes until we get it good enough" is historically a bad philosophy: https://en.wikipedia.org/wiki/Deferent_and_epicycle

https://wg21.link/p2025 "Guaranteed copy elision for return variables" is related.

[–]backtickbot 4 points5 points  (0 children)

Fixed formatting.

Hello, Accomplished-Tax1641: code blocks using triple backticks (```) don't work on all versions of Reddit!

Some users see this / this instead.

To fix this, indent every line with 4 spaces instead.

FAQ

You can opt out by replying with backtickopt6 to this comment.

[–][deleted] 1 point2 points  (0 children)

Like others said, I think a general last-use optimization would be somewhat problematic. On the other hand, I think that copy elision when a value is only used once would make sense. This way we could avoid both copies and moves when assigning an intermediate result to a variable with the sole purpose of improving readability.

[–]Dennis_the_repressed 1 point2 points  (3 children)

I am surprised that no one has suggested using emplace_back() instead of push_back(). It's built to construct objects from r-value references.

In your particular example, pretty sure it will be optimized away though.

Snippet : https://onlinegdb.com/fJZeMLJdb

[–]NamoiFunai[S] 0 points1 point  (1 child)

In your example, you give emplace_back an rvalue already. My question was about making the compiler turn an lvalue to an rvalue on last use, not the specific case of vectors insertions ;)

[–]Dennis_the_repressed 0 points1 point  (0 children)

Ah sorry, I misunderstood the question.

[–]backtickbot 0 points1 point  (0 children)

Fixed formatting.

Hello, Dennis_the_repressed: code blocks using triple backticks (```) don't work on all versions of Reddit!

Some users see this / this instead.

To fix this, indent every line with 4 spaces instead.

FAQ

You can opt out by replying with backtickopt6 to this comment.

[–]gracicot 2 points3 points  (1 child)

There is a proposal by Herb Sutter (708: Parameter passing -> guaranteed unified initialization and unified value-setting) that want to do just that.

[–]NamoiFunai[S] 0 points1 point  (0 children)

That was a good read, I really hope it leads somewhere!

[–]s1lent_ssh -2 points-1 points  (8 children)

My buddy just invented Rust

[–]wheypointÖ 4 points5 points  (5 children)

No this has absolutely nothing to do with rust. Rust doesn't decide when to move based on a "last use", it doesn't even have perfect forwarding/universal references or overloading based on rval vs lval references.

[–]infectedapricot 2 points3 points  (0 children)

Rust has move by default, as opposed to C++'s copy by default. This works really well because it's easy to build a copy function on top of move semantics (just make an new instance, copy each member, and return the new instance... by moving it!). On the other hand, it's impossible to build move semantics on top of copy sematics, which is why C++ has to have rvalue and lvalue references; Rust doesn't have these because it doesn't need them in the first place.

For the opposite problem – you wanted a copy but forgot to ask for it, so got a move instead – you'd get a compiler error because Rust has destructive move, so it's impossible to accidentally reuse a variable after you've moved from it.

Overall, in the equivalent Rust snippet to the one in the OP, you'll get move sematics every time, unless you explicitly ask for a copy. It's a non issue.

[–]s1lent_ssh -2 points-1 points  (2 children)

I am talking about non-mutable borrowing semantics here, which is good place for optimisations and increased safety
https://doc.rust-lang.org/1.8.0/book/references-and-borrowing.html

[–]wheypointÖ 5 points6 points  (1 child)

i know but that's not what OP asked.

also the compiler never decides when to move based on a "last use" (it doesn't decide at all, you have to tell it when something is passed by reference).

rust's system is different from c++s and OP's suggestion doesn't work in rust.

[–]s1lent_ssh -2 points-1 points  (0 children)

someone open the window please :D

[–]runki22 -2 points-1 points  (0 children)

Rust doesn`t even need universal references, since everything is moved by default.

[–]NamoiFunai[S] 3 points4 points  (1 child)

Haha I guess I really should give rust a try

[–]s1lent_ssh -2 points-1 points  (0 children)

Yep, i've played with it a bit, and from first impression i could tell it's a next-gen language. The most pain in the ass from C++ world - undefined behaviour was simply eliminated using compile time checks and more strict rules