Remix.run Logo
Dylan16807 5 days ago

> The destructor already has to deal with that.

That's partly true, partly circular. Because moves work this way, it's harder to make a class that doesn't have empty states, so I don't design my class to avoid empty states, so the destructor has to handle them.

motorest 4 days ago | parent | next [-]

> That's partly true, partly circular.

I don't think there is anything "partly" about it being true. A moved-from object is expected to remain valid and preserve class invariants. If you wrote a class whose objects fails to remain valid after being moved,you wrote bugs into your code.

> Because moves work this way, it's harder to make a class that doesn't have empty states, so I don't design my class to avoid empty states, so the destructor has to handle them.

You are not required to implement an empty state. You are only required to write your classes so that after moving an object it remains valid. You are free to specify what this means to your classes, and can be anything from leaving the object as if it was default initialized or have literally a member variable such as bool moved. It's up to you. In C++'s perspective as long as your moved-from object can be safely destroyed them it's all good. Anything else is the behavior you chose to have, and bugs you introduced.

mort96 4 days ago | parent | prev | next [-]

It's not like it's the only part of the language that mandates a default constructor though. There are plenty of situations where default-constructible types are desirable. Even simple things like having a non-default-constructible type in a map is awkward.

masklinn 4 days ago | parent [-]

> It's not like it's the only part of the language that mandates a default constructor though

It’s… not a part of the langage which mandates a default ctor in the first place.

motorest 4 days ago | parent | next [-]

> It’s… not a part of the langage which mandates a default ctor in the first place.

Why should it, tough? Think about it. The goal of move semantics is performance, mainly avoiding to copy/initialize expensive objects using a standard syntax. Why do you believe it would be a good idea to force constructors when they can very well be the reason why move should be used?

masklinn 3 days ago | parent [-]

Did you reply to the wrong comment?

mort96 4 days ago | parent | prev [-]

It doesn't, but it does mandate that the object has some "empty state". If you have an empty state you might as well have a default constructor which initializes the object to that empty state.

masklinn 3 days ago | parent [-]

moved-from objects are not in an empty state but in an unspecified state, they are only required to be destructible, every other operation can be disallowed. That is not a useful state for default construction. Thus being movable does not imply defaulting is any sort of good idea.

The other way around makes more sense, but even then it is not systematic, if default construction is costly (allocation, syscall, …) then you don’t want to do that for a moved-from object which will just be destroyed, which is the fate of most.

mort96 3 days ago | parent [-]

> moved-from objects are not in an empty state but in an unspecified state, they are only required to be destructible, every other operation can be disallowed. That is not a useful state for default construction. Thus being movable does not imply defaulting is any sort of good idea.

This is only true for standard library objects. The C++ standard specifies that e.g std::unordered_map will be in a "valid but unspecified state" when moved from. You can define your own classes to behave however you want, including defining a moved-from object to be identical to a default-constructed object.

Regardless, you're missing the point. Even if the standard specified that every moved-from object can only be destructed and every other use was UB, you still need the moved-from object to be in some empty state. Your move constructor and move operator= needs to put the moved-from object into some state where it doesn't own any resources but can be safely destructed. There's typically little reason to not make this "doesn't own any resources" state available through a default constructor.

> The other way around makes more sense, but even then it is not systematic, if default construction is costly (allocation, syscall, …) then you don’t want to do that for a moved-from object which will just be destroyed, which is the fate of most.

If your class can be in some "empty" state that doesn't own any resources and doesn't require syscalls to construct, you want this to be both the default constructed state and the moved-from state. Default-constructible objects end up getting default-constructed then overwritten all the time in C++, such as by the common pattern 'my_unordered_map["foo"] = MyClass(...)' which will first default-construct a value and then call its move operator=.

spacechild1 5 days ago | parent | prev [-]

Please give me an example for a class that needs to handle empty state in the destructor only because of move operations. These exist, but IME they are very rare. As soon as you have a default constructor, the destructor needs to handle the case of empty state.

ninkendo 5 days ago | parent | next [-]

It’s not just the destructor you have to worry about, it’s all of the state accessible to callers.

If you have any type that represents validated data, say a string wrapper which conveys (say) a valid customer address, how do you empty it out?

You could turn it into an empty string, but now that .street() method has to return an optional value, which defeats the purpose of your type representing validated data in the first place.

The moved-from value has to be valid after move (all of its invariants need to hold), which means you can’t express invariants unless they can survive a move.

It is much better for the language to simply zap the moved-from value out of existence so that you don’t have to deal with any of that.

spacechild1 5 days ago | parent [-]

First, one shouldn't use a moved-from object in the first place (except for, maybe, reassigning it).

Second, why can't the .street() method simply return an empty string in this case?

> The moved-from value has to be valid after move (all of its invariants need to hold)

The full quote from the C++ standard is: "Unless otherwise specified, such moved-from objects shall be placed in a valid but unspecified state" AFAIK, it only makes such requirements for standard library types, but not for user defined types. Please correct me if I'm wrong.

ninkendo 5 days ago | parent [-]

> First, one shouldn't use a moved-from object in the first place (except for, maybe, reassigning it).

It still requires you to come up with somethkng to do to the old value in the move constructor. What would you do in the ValidatedAddress case? Set a flag in the struct called “moved_from” and use that to throw an exception if it’s ever used? Wouldn’t it be nice if you just didn’t need to worry about it?

> Second, why can't the .street() method simply return an empty string in this case?

In this example I’m referring to a type that represents a “validated” address, so, one that has already passed checks to make sure the street isn’t empty, etc. (it’s the whole “parse, don’t validate” idea, although I’ve never understood why the word “parse” is used when I would’ve just called it “validate just once”.)

It is an extremely useful concept for your type system to represents invariants in your data like this. Having to make every type contain an “empty” case, just to make the language’s move semantics work, pokes an enormous hole through this idea.

> AFAIK, it only makes such requirements for standard library types, but not for user defined types

It makes the requirement because the compiler is not going to stop anyone from using the moved-from value, so you have to think of something to do in the move constructor. You can pinky-swear to never use the moved-from value in your own code (and linters can help here) but the possibility still exists, so it must be solved for.

spacechild1 5 days ago | parent | next [-]

> Having to make every type contain an “empty” case, just to make the language’s move semantics work, pokes an enormous hole through this idea.

Nobody says that the invariants must hold after the object has been moved-from! The only thing you need to do is make sure that the destructor can run and do the right thing.

> You can pinky-swear to never use the moved-from value in your own code (and linters can help here) but the possibility still exists, so it must be solved for.

Letting the program crash would be a valid solution (for your own types).

For me the issue with C++ move semantics is not so much that you have to add special logic to your classes, but the fact that moved-from objects can be accessed in the first place. In this respect I definitely agree that destructive moves are better.

motorest 4 days ago | parent | prev [-]

> Wouldn’t it be nice if you just didn’t need to worry about it?

Do you worry about it? I mean, to begin with, do you purposely try to reuse objects that you explicitly moved? If you do, in the very least you can be lazy and reassign a newly constructed object right after you explicitly move its contents, but I don't see any reason that would justify such a thing.

Can you point out what you feel is the scenario that worries you the most?

3 days ago | parent [-]
[deleted]
tialaramex 5 days ago | parent | prev | next [-]

This means C++ is riddled with types that have unrelated "I'm empty" state inside them rather than this being relegated to a separate wrapper type. It's Tony's Billion Dollar Mistake but smeared across an entire ecosystem.

The smart pointer std::unique_ptr<T> is an example of this, sometimes people will say it's basically a boxed T, so analogous to Rust's Box<T> but it isn't quite, it's actually equivalent to Option<Box<T>>. And if we don't want to allow None? Too bad, you can't express that in C++

But you're right that C++ people soldier on, there aren't many C++ types where this nonsense unavoidably gets in your face. std::variant's magic valueless_by_exception is such an example and it's not at all uncommon for C++ people to just pretend it can't happen rather than take it square on.

spacechild1 5 days ago | parent | next [-]

> This means C++ is riddled with types that have unrelated "I'm empty" state

Again, these cases are still rare. Most classes either don't require user-defined move operations, or they have some notion of emptiness or default state.

> And if we don't want to allow None? Too bad, you can't express that in C++

That's actually a good example! Nitpick: you can express it in C++, just not without additional logic and some overhead :)

7jjjjjjj 4 days ago | parent [-]

>you can express it in C++, just not without additional logic and some overhead :)

How?

spacechild1 4 days ago | parent [-]

E.g. with a boolean member or by setting a bit in the pointer value.

steveklabnik 5 days ago | parent | prev [-]

(And that difference leads to an ABI difference that makes it not a zero overhead abstraction in the way that Box is…)

spacechild1 5 days ago | parent [-]

Great point! Chandler Carruth explained this in one of this cppcon talks: https://youtu.be/rHIkrotSwcc?t=1047

sgsjchs 5 days ago | parent | prev [-]

A socket.

spacechild1 5 days ago | parent [-]

How so? Doesn't your socket class have a default constructor and a notion of open and closed?

sgsjchs 5 days ago | parent [-]

If the moves were destructive, I'd design it to have the default constructor call `::socket` and destructor call `::close`. And there wouldn't be any kind of "closed" state. Why would I want it?

spacechild1 5 days ago | parent [-]

Your socket class would have no default constructor? And you would never want to close the socket before the object's lifetime ends? Really?

sgsjchs 4 days ago | parent | next [-]

In this case, I would want the address family and protocol to be statically known, so it would have default constructor. But for example, a file might not have one, sure. As for closing before lifetime ends, why? I can just end lifetime. Wrap it in an optional if the type system can't figure it out like with a struct member.

spacechild1 4 days ago | parent [-]

> so it would have default constructor.

And what's the underlying value of such a default constructed socket? I assume it would be -1 resp. INVALID_SOCKET, in which case the destructor would have to deal with it.

> Wrap it in an optional if the type system can't figure it out like with a struct member.

So you essentially must wrap it in an optional if you want to use it as a member variable. I find this rather pointless as sockets already have a well-defined value for empty state (-1 resp. INVALID_SOCKET). By wrapping it in a optional you are just wasting up to 8 bytes.

Sure, you can implement a socket class like that, but it's neither necessary nor idiomatic C++.

sgsjchs 4 days ago | parent [-]

> And what's the underlying value of such a default constructed socket? I assume it would be -1 resp. INVALID_SOCKET

No, as explained, the default value would be the result of `::socket` call, i.e. a fresh OS-level socket.

> So you essentially must wrap it in an optional if you want to use it as a member variable.

No, you only must wrap it if you really want this closed state to exist.

> Sure, you can implement a socket class like that, but it's neither necessary nor idiomatic C++.

Obviously. Because the moves are not destructive. If they were, this design would be superior. And the wasted space for optional is solvable, just like for non-nullable pointers.

spacechild1 4 days ago | parent [-]

> If they were, this design would be superior.

I see how destructive moves would slightly simplify the implementation, but what difference would it make apart from that? (Don't get me wrong, I totally think that destructive moves are a good idea in general, I just don't see the qualitative difference in this particular case.)

> And the wasted space for optional is solvable, just like for non-nullable pointers.

In the case of non-nullable pointers the library author knows that they can use NULL as a sentinel value and write a corresponding specialization. But what could you possibly do with an arbitrary user-defined class?

sgsjchs 4 days ago | parent [-]

> what difference would it make

The same difference as making pointers always non-nullable and reintroducing nullability via an optional wrapper only when semantically appropriate.

> what could you possibly do with an arbitrary user-defined class

Just add some customization points to std::optional so that users can define which value of the class to treat as noneopt internally.

spacechild1 3 days ago | parent [-]

> The same difference as making pointers always non-nullable and reintroducing nullability via an optional wrapper only when semantically appropriate.

Again, I don't see what this has to do with destructive moves. If you want a socket class that always refer to an open socket, you can already do that. Same for non-nullable pointer wrappers. Conversely, destructive moves don't prevent you from implementing a socket class with a close() method. These concepts are really orthogonal.

> Just add some customization points to std::optional so that users can define which value of the class to treat as noneopt internally.

How is this supposed to work? The very point of your socket class is that it always contains a valid socket handle. Once you introduce a sentinel value, you are back to square one. If the optional class is able to construct a socket with the sentinel value, so is the user.

sgsjchs 3 days ago | parent [-]

> Again, I don't see what this has to do with destructive moves. If you want a socket class that always refer to an open socket, you can already do that.

Technically you can, but it's unreasonable to create an os-level socket just to put into the moved-out object where it will be immediately destroyed again. This is not an issue when the moves are destructive.

> How is this supposed to work? The very point of your socket class is that it always contains a valid socket handle. Once you introduce a sentinel value, you are back to square one. If the optional class is able to construct a socket with the sentinel value, so is the user.

That's not true. The sentinel value need not be exposed in the public interface of the class, it can only be accessible via the customization point of the optional.

spacechild1 3 days ago | parent [-]

> Technically you can, but it's unreasonable to create an os-level socket just to put into the moved-out object where it will be immediately destroyed again. This is not an issue when the moves are destructive.

No, the class can use a sentinel value internally only to mark moved-from objects. That's exactly where we actually started the conversation. That's why I said that destructive moves would only somewhat simplify the move operations, but not make a qualitative difference (in this area).

> The sentinel value need not be exposed in the public interface of the class, it can only be accessible via the customization point of the optional.

Since the optional would need to construct an instance with the sentinel value, I thought that the "sentinel" constructor must be public. However, you might be right that one could write a template specialization that contains the template argument as a friend class. In this case you could use a private constructor. Note that the destructor still has to handle the sentinel value... But I guess this is just something you have to accept.

sgsjchs 3 days ago | parent [-]

> No, the class can use a sentinel value internally only to mark moved-from objects. That's exactly where we actually started the conversation.

The issue is that the "moved-from" state is exposed to the user when the moves are not destructive. The author of the class has to consider behavior for every method in sentinel state, even when it's just to assert that the state isn't sentinel or "lol it's UB". And the user has to be careful not to accidentally misuse an object in sentinel state. Just like how every time you touch a nullable pointer you have to consider if it can be null and what to do in that case. As long as the sentinel state is exposed at all (via non-destructive move), there is little gain in not providing full support for it. However, with destructive moves the sentinel value either doesn't exist at all or only exists completely internally as an optimization, and all this mental overhead disappears.

spacechild1 3 days ago | parent [-]

I see your point. Just a few things:

1. This is only relevant when using such class as a local variable. Member variables are typically not moved-from.

2. In my understanding the user has the freedom to specify what constitutes a "valid but unspecified state" and it would be perfectly ok to mandate that anything you can do with a moved-from object is to either destroy or reassign it.

3. The problems with the state of moved-from objects from the perspective of a library author could have been prevented simply by imposing stricter requirements in the standard (e.g. every usage except destruction, and possible reassignment, shall be UB).

4. With all the issues you've pointed out, it is still be perfectly possible and reasonable to design a socket class your way (= no closed socket state) in C++, yet somehow most people seem to prefer open() and close() methods instead of modelling the state with an optional. Even in the presence of destructive moves, I don't think that one way is necessarily better than the other and it is mostly a matter of culture and personal preference.

All the being said, I definitely agree that destructive moves are good thing, in particular if the compiler prevents you accidentally accessing moved-from objects (which is a mistake that is very easy to make in C++).

sgsjchs 3 days ago | parent [-]

Indeed, the "valid but unspecified state" refers only to some types defined in the he standard library. It essentially means that you can only call methods which have no preconditions and don't depend on what that state is, e.g. assignment or destruction, or something like string::clear or vstring::assign if you want defined outcomes. In general each type is free to guarantee whatever the author wants about the moved from state, e.g. moved-from std::unique_ptr is always null.

7jjjjjjj 4 days ago | parent | prev [-]

With destructive moves, you can end an object's lifetime whenever you want.

spacechild1 4 days ago | parent [-]

How would I use such a socket class as a member variable? How do I reopen the socket?

sgsjchs 4 days ago | parent [-]

Reopen by constructing and assigning a new socket.

spacechild1 4 days ago | parent [-]

So I essentially have to wrap it in something like std::optional. Well, that's certainly one way to write a socket class, but I'd say it's not idiomatic C++. (I have never seen a socket class being implemented like that.)

sgsjchs 4 days ago | parent [-]

You don't need optional in this case, the assignment would just destroy the old socket and immediately move the new one in its place.

spacechild1 3 days ago | parent [-]

Well, reopening a socket implies that I have manually closed the socket, which does require an optional with your implementation.