Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SUGGESTION] Move by default for in parameters? #114

Closed
tylerjw opened this issue Nov 12, 2022 · 18 comments
Closed

[SUGGESTION] Move by default for in parameters? #114

tylerjw opened this issue Nov 12, 2022 · 18 comments
Assignees

Comments

@tylerjw
Copy link

tylerjw commented Nov 12, 2022

Ever since the introduction of move semantics a ton of boilerplate code has been written to enable moving the ownership of variables into arguments to functions or constructors.

Have you considered making in-parameters move by default with explicit syntax for creating a copy when the user would prefer to not move the ownership of a variable into the scope of a function?

In many cases moving into a parameter is the correct choice but the current syntax for doing that correctly is difficult to learn and use correctly.

One side-effect of doing this would mean that to maintain safety the compiler would need to detect when someone used a variable after they moved it. clang-tidy does have a check for this: https://clang.llvm.org/extra/clang-tidy/checks/bugprone/use-after-move.html

@filipsajdak
Copy link
Contributor

Hm... move by default will make simple cases unintuitive.

E.g. code:

i := 42;
fun(i);
gun(i);
g := i + 1;

will be bad... or it need to be forbidden.

Above code is fine from mathematics point of view - we shall support that. All the other cases needs to stand out e.g. by additional keywords:

i := 42;
fun(out i); // i will be out parameter - expect it will be changed
gun(i);
g := i + 1;

@tylerjw
Copy link
Author

tylerjw commented Nov 12, 2022

The suggestion of moving by default does look silly with trivial types like an integer. Maybe there should be some "trivially copyable" kind of types that get copied by default, where all others get moved by default?

This is a behavior of Rust that I find results in a lot less boilerplate than the C++ code I write. I know there is resistance to copying ideas from Rust, but this is one that, enabled by the type system, is safe, ergonomic, and performant by default. When writing C++ code, authors of types have to create a lot of boilerplate code to enable users to use move semantics. This isn't easy to learn to do correctly and makes for a lot of noisy syntaxes at both the function definition and the call site to enable moving ownership.

@switch-blade-stuff
Copy link

Move by default is what Rust does, but I don't see it to be a good idea for cppfront. In Rust every type is considered "trivial" and deep-copies are explicit. Meanwhile C++ does the opposite where copies are the default behavior and anything else is explicit.

Additionally, making trivial types be passed by-copy and non-trivial be passed by move in some cases and not in other will be very confusing and lead to unintended hurdles IMO. It should either be default-copy or default-move not both.

Also it does not help that in C++ there is no such thing as a destructive move yet, and that move operations can be overloaded, so default-move will loose that advantage (shallow memcpy) as well.

@JohelEGP
Copy link
Contributor

Have you considered making in-parameters move by default with explicit syntax for creating a copy when the user would prefer to not move the ownership of a variable into the scope of a function?

You should get that when the argument is a last use.

@tylerjw
Copy link
Author

tylerjw commented Nov 13, 2022

You should get that when the argument is a last use.

Is there anything about C++ that makes it easy for compilers to track where the last use of a variable is? My impression is that even when this would be safe, it is not allowed unless explicitly asked for by the code. What happens to references to variables when the variable is moved?

@tylerjw
Copy link
Author

tylerjw commented Nov 13, 2022

Also it does not help that in C++ there is no such thing as a destructive move yet

By referencing the clang-tidy check, I was implying that this would require some static analysis in the compiler to be safe. Destructive moves could solve that problem.

@JohelEGP
Copy link
Contributor

Is there anything about C++ that makes it easy for compilers to track where the last use of a variable is?

Cppfront does it. I think you can see that on the CppCon talk on the README.

@tylerjw
Copy link
Author

tylerjw commented Nov 13, 2022

Cppfront does it. I think you can see that on the CppCon talk on the README.

I'm sorry, I don't remember that from the talk. How does that work? Is cppfront doing ownership and lifetime analysis for variables?

@JohelEGP
Copy link
Contributor

#77 (comment) has a link into that.

@hsutter
Copy link
Owner

hsutter commented Nov 13, 2022

Quick ack while at the ISO C++ meeting (which just finished a few hours ago so I'm briefly looking in here):

Is there anything about C++ that makes it easy for compilers to track where the last use of a variable is?

See this 1-min clip from the CppCon 2022 talk.

For more details see the longer treatment of parameter passing in the CppCon 2020 talk starting at 16:11.

@tylerjw
Copy link
Author

tylerjw commented Nov 13, 2022

Feel free to close this, I did not understand how the in/out/inout/forward parameter annotations would address the pain point of all the boilerplate around moving parameters, but now I think I understand much better.

Thank you for the explanations; I could see how this would make this part of the language much nicer. My initial thought of move-by-default comes from writing Rust recently. One thing that it relies on for making move-by-default work well though is the Copy trait which I don't see any straightforward way to replicate in C++.

Having clear syntax to tell the compiler how you intend to use a parameter seems like it would be a really nice abstraction.

@fluffinity
Copy link

C++ technically has the ability to query whether a type can be trivially copied via the std::is_trivially_copyable type trait. Such types can be copied via a simple memcpy. As things look at the moment this logic would have to be to re-implemented within the cppfront compiler.

As this would mean having logic to query certain attributes of types within cppfront itself such a feature would go in hand with the support for custom structs/classes. When the compiler has to support user written types it needs to reason about them anyways.

@switch-blade-stuff
Copy link

@fluffychaos

As this would mean having logic to query certain attributes of types within cppfront itself

What do you mean by that? You can already use standard type traits with cppfront. Or did you mean something else?

Also, the main difference about value semantics in C++ and Rust is that Rust does memcpy by default, and only does deep copies if you ask it to, meanwhile C++ defaults to deep copies which may become memcpy in the end, but not required to. And since both copy and move constructors can be overloaded in C++, you cannot really rely on it being a simple memcpy all the time.

@fluffinity
Copy link

@switch-blade-stuff

What do you mean by that? You can already use standard type traits with cppfront. Or did you mean something else?

Currently cppfront will just generate the C++ code containing usage of these type traits but will not recognize them specifically. It does not know about trivially copyable types. In order to safely allow for automatic copies in the form of memcpy you need this knowledge.

Also, the main difference about value semantics in C++ and Rust is that Rust does memcpy by default, and only does deep copies if you ask it to, meanwhile C++ defaults to deep copies which may become memcpy in the end, but not required to. And since both copy and move constructors can be overloaded in C++, you cannot really rely on it being a simple memcpy all the time.

This is why cppfront needs to reason about a type being trivially copyable. It is the equivalent to Rusts Copy trait. Both indicate to the compiler that instances of these types can be cheaply copied and in C++ it also means there is no custom copy logic involved making a shallow memcpy sufficient. This way the compiler can know when a move can become a copy instead.

The example given in the above comment would become ergonomic as the numeric primitive types are all trivially copyable and the compiler would make copies instead of moves then. If i was a not trivially copyable it would not compile again as then the compiler would use moves. This is the desired outcome.

@switch-blade-stuff
Copy link

@fluffychaos I understand what you meant now, but unfortunately i don't think it would be easily/any time soon achievable in cppfront right now, since that would essentially require cppfront to be a C++ compiler itself, and not only for Cpp2, but also Cpp1, since it would have to understand Cpp1 type system as well.

Right now, the best thing we could do would be to generate some kind of wrapper that checks for std::is_trivially_move_constructible<T>, in which case it uses value semantics (giving you the memcpy since it is trivial), and otherwise uses rvalue semantics.

@fluffinity
Copy link

I understand what you meant now, but unfortunately i don't think it would be easily/any time soon achievable in cppfront right now, since that would essentially require cppfront to be a C++ compiler itself, and not only for Cpp2, but also Cpp1, since it would have to understand Cpp1 type system as well.

I agree. This would take a lot of work to support. Your idea may be worth implementing for generic cpp2 code. For non-generic cpp2 code however I don't think this is a good idea as these checks would clutter the generated cpp1 code with checks for concrete types. So you would end up with C++ code you would not write this way otherwise since you would use the semantics best suited for your concrete type. It would go against the idea that you can simply take the generated code and work with that.

At least in the resulting binary these checks would not appear and you get the optimized semantics as you can do these checks in a constexpr environment.

But before we stray away from the original topic of this issue too much I would say it makes sense to open a new one for this idea.

@hsutter
Copy link
Owner

hsutter commented Nov 26, 2022

Yes, this is on the list... I'm considering implicit move from definite last use for in parameters, and initially implemented that but hit a small problem that I now can't remember. I'm doing it now for copy parameters.

I'll close this issue now because it's on the list of things I'm thinking about and I'll do it if it makes sense. Thanks!

@hsutter hsutter closed this as completed Nov 26, 2022
@hsutter hsutter self-assigned this Nov 26, 2022
@jcanizales
Copy link

C++ technically has the ability to query whether a type can be trivially copied via the std::is_trivially_copyable type trait. Such types can be copied via a simple memcpy. As things look at the moment this logic would have to be to re-implemented within the cppfront compiler.

Hopefully, support for compile-time code that can iterate and query class definitions comes before that.

Eigen's dense matrices are trivially copyable in the sense that a memcpy will always work with them. But std::is_trivially_copyable is false for them, because they need to define constructors for a number of (good) reasons. I can define a trait template, IsActuallyTriviallyCopyable<T>, with specializations to return true for Eigen dense matrices. But I can't make my trait work for classes that contain such a matrix, because I can't make it recursive on the members of T.

With compile-time code that can query class definitions, std::is_trivially_copyable can be easily written in user code, so the compiler doesn't have to implement it. And I can extend it or write my own to make it work on Eigen matrices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants