Hacker News new | ask | show | jobs
by spacechild1 290 days ago
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.
3 comments

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.

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.

> 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.

> 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.

> 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?

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.

> 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 :)

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

How?

E.g. with a boolean member or by setting a bit in the pointer value.
(And that difference leads to an ABI difference that makes it not a zero overhead abstraction in the way that Box is…)
Great point! Chandler Carruth explained this in one of this cppcon talks: https://youtu.be/rHIkrotSwcc?t=1047
A socket.
How so? Doesn't your socket class have a default constructor and a notion of open and closed?
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?
Your socket class would have no default constructor? And you would never want to close the socket before the object's lifetime ends? Really?
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.
> 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++.

With destructive moves, you can end an object's lifetime whenever you want.
How would I use such a socket class as a member variable? How do I reopen the socket?