5 ms·
Does anyone know what the move / copy semantics are in Carbon? I can't figure it out from the docs (admittedly I have not looked hard at all). I would say the
by quietbritishjim 4y ago
Does anyone know what the move / copy semantics are in Carbon? I can't figure it out from the docs (admittedly I have not looked hard at all).
I would say the number one weakness of C++ is its model of copy by default and often by surprise. For example: f(std::move(x)) will copy x if x is const or f takes its value by const ref - rather than a compiler error as you might reasonably expect (if you don't know the C++ reference model inside out).
In Rust, moves are destructive (which simplifies implementions - you don't need an "empty" state) and always bitwise (which is usually fine, so long as they're destructive); and copies are always explicit so you don't have the above problem. I personally think that is all more useful than the borrow checker.
If Carbon has the same bizarre move/copy rules as C++ then the effort is wasted IMO.
- yakubin 4y ago> In Rust, moves are destructive (which simplifies implementions - you don't need an "empty" state) and always bitwise (which is usually fine, so long as they're destructive); and copies are always explicit so you don't have the above problem. I personally think that is all more useful than the borrow checker. Without the borrow checker, this "move-by-default, move is memcpy" semantics would break all hell loose. Dangling pointers/references would be everywhere. I like this part of Rust, but I'm not sure if it can work in a non-GC, no-borrow-checker language.
- quietbritishjim 4y agoYou would need to add the rule that you can't move from a variable in only one branch of an if statement or from any sort of loop. Like a really primitive compile time borrow checker. It seems to me that would be enough, but maybe I haven't thought it through properly, or maybe you'd end up having to copy a lot more often than you want to (or use optional half the time, which might defeat the point a bit).
- jcranmer 4y agoYou don't need a full-on borrow-checker to make move-by-default safe. If you can track whether or not a variable has been (definitely) initialized, you can track whether or not a variable has been moved from. It's the same basic logic. And there are languages without full borrow checkers that can do that--Java is the first one that comes to mind.
- yakubin 4y agoDon't you need to know when all borrows end? E.g. here: fn main() { let x = [1, 2, 3]; println!("{:?}", x); let y = x.map(|x| x % 2 == 0); println!("{:?}", y); } x is borrowed by println (or rather by code that println expands to) and then it is moved by array::map. The compiler needs to know that before x.map(), x is not borrowed anymore. Otherwise it could be that println actually stored a reference to x somewhere and now you have a dangling reference possibly breaking type-safety (referring to an array of bools through a reference to array of ints, if the map is implemented as an in-place operation).
- quietbritishjim 4y agoSure but that is already a risk in C++ at the moment. void foo() { if (true) { Foo f; stores_a_reference_to_foo(&f); } uses_stored_reference_to_foo(); } I see that destructive moves would in theory introduce more opportunities for that, but I don't really see it making a big difference in practice - routines that take a pointer/reference and hold onto them (without taking ownership) already need a lot of care in C++.
- jcranmer 4y agoWithout a borrow checker, you won't be able to eliminate dangling reference issues. But that's an orthogonal issue to move safety.
- jeffbee 4y agoThe move situation would be less confusing to newcomers if they had called it what it really does, std::become_rvalue_reference or something.
- quietbritishjim 4y agoI agree that it should have been named better (rvalue_cast might have worked) but that wouldn't have solved the core problem: Does f(std::become_rvalue_reference(x)) make a copy of x? It's more obvious that it might but I still lack the facility to make it cause a compile time error if it would (except changing x's class to disallow any copies at all).
- jeffbee 4y agoIn general you can't answer these questions by reading the call site. You have no idea what kinds of intermediate functions are implied by f(x). Maybe f() doesn't take an X at all, maybe it takes a Y and there's an implicit Y::Y(const X&) hanging around, or perhaps there is an X::operator Y(). You can only understand f(x) by reading the definitions of f, its argument type, and X.
- gpderetta 4y agoalternatively move should have been a real move: template<class T> T std::move(T&& t) { return T(static_cast<typename std::remove_reference<T>::type&&>(t)); } But it would have had performance implications.
- quietbritishjim 4y agoInteresting - that prevents one of the two cases. Given fn(const Foo&) (or fn(Foo&)), your implementation would stop fn(move(x)) from compiling. But given fn(Foo) and const Foo x, your implementation would still allow fn(move(x)) to compile and perform a copy.
- gpderetta 4y ago
- est31 4y ago> In Rust, moves are destructive (which simplifies implementions - you don't need an "empty" state) and always bitwise (which is usually fine, so long as they're destructive); and copies are always explicit so you don't have the above problem. Rust has implicit copies too, e.g. if you do: let v = 2u16; foo(v); then v is being copied. What Rust does not have is implicit deep copies. The auto-copying depends on the type and is only done for types that don't have a custom destructor, or to be more precise, implement the Copy trait. So a big array of integers gets copied (because it implements the Copy trait), but a Vec gets moved (because of its custom destructor there is no Copy trait implementation).
- mleonhard 4y agoIn Rust, types implement the Copy trait and the Drop (destructor) trait independently. Vec could easily implement Copy. It doesn't because copying a Vec requires an expensive heap allocation, which can surprise users when it happens implicitly.
- est31 4y agoNo you can't, there is a specific error for that. You can try it: struct Foo; impl Copy for Foo {} impl Drop for Foo { fn drop(&mut self) { // ... } } It gives the error: error[E0184]: the trait `Copy` may not be implemented for this type; the type has a destructor --> src/main.rs:3:1 | 3 | impl Copy for Foo {} | ^^^^^^^^^^^^^^^^^^^^ `Copy` not allowed on types with destructors See also the "When can't my type be Copy?" section of the Rust documentation: https://doc.rust-lang.org/1.62.0/std/marker/trait.Copy.html#when-cant-my-type-be-copy https://doc.rust-lang.org/1.62.0/std/marker/trait.Copy.html#...
- quietbritishjim 4y agoSure, I knew about the Value trait but I thought it wasn't relevant for the general point (and I thought that sentence had too many parenthesised clarifications already!).
- tialaramex 4y ago
- jcranmer 4y ago> Does anyone know what the move / copy semantics are in Carbon? I can't figure it out from the docs (admittedly I have not looked hard at all). I read the documentation and had the very same question. My notes from the announcement thread had this line in it: > * Not clear from the main design document is how the distinction--if any--between trivial/nontrivial copy/move types work. There appears to be an explicit move operator (with obvious syntax ~x), so support for nontrivial move or immovable is better than Rust already, but the avoidance of a NRVO setup still makes me wonder what the actual story is here. Given that Carbon copies the signed-no-overflow/unsigned-may-overflow distinction from C/C++, it wouldn't surprise me if they also copy the move/copy rules from C++, as those are both rather fundamentally baked into C++ (which means you have to deal with it for automatic translation), even if they are pretty objectively confusing.