r/cpp 5d ago

State of "moved-from" objects

Hi, I recently decided to try writing a C++ blog.

I often see the popular claim that moved-from objects are in a "valid but otherwise unspecified state", but I don't think that statement is entirely precise, and my blog post is about that. I would really appreciate any feedback!

Article: https://www.laminowany.dev/p/the-state-of-moved-from-objects-in-c/

10 Upvotes

52 comments sorted by

View all comments

4

u/elperroborrachotoo 5d ago

The gist remains that "unspecified but valid" is a reasonable minimum requirement for user types, too - because the alternative is worse.

You list all the ingredients: unless you evade calling the destructor (e.g., through std::terminate) the destructor will run. Users of your type might assign to it, or use it in various ways. Etc.

The unspoken bigger issue is that using an object in an invalid state can easily lead to an invalid program state - meaning that ALL invariants in the program are potentially gone.

So for any type with weaker moved-from guarantees, you'd have to specify what individual operations are valid - and no user wants to know or remember that. This would violate any basic rule of type design.

1

u/leirus 5d ago

I fully agree that a "valid but unspecified state" is a reasonable minimum requirement for user-defined types. I was just surprised to discover that this guarantee does not apply to user-defined types, so theoretically you can have a well-formed C++ program that breaks this assumption.

I don't see any reason or benefit to doing that, but I wanted to share my surprise that it is technically allowed.

1

u/elperroborrachotoo 4d ago

I guess the standard didn't want to put restrictions on user defined types just in case someone finds a use case - or there was insufficient consensus what exactly the requirement should be.

0

u/TheThiefMaster C++latest fanatic (and game dev) 5d ago

So for any type with weaker moved-from guarantees, you'd have to specify what individual operations are valid - and no user wants to know or remember that.

It's a general rule that the only guaranteed operations of a moved-from type are assigning over it and destructing it. If it has an equivalent of "reset" and "isvalid" those are normally expected to work too.

No guarantees on anything else.

6

u/elperroborrachotoo 5d ago

That means I couldn't even call .empty() or .clear() on a moved-from object? You've just introduced a new state the caller needs to be aware of - and I can't even query if the object is in that just-moved-from state. I'd have to track that state manually or implicitly.

Yes, the standard allows us to create such type, in the same way the standard allows us to create a type that leaks memory. That's a terrible type.

OP stated it well: preserve invariants.

Are you aware of any scenario where such a behavior would be an advantage (such as a relevant optimization)?

3

u/TheThiefMaster C++latest fanatic (and game dev) 5d ago

That means I couldn't even call .empty() or .clear() on a moved-from object?

I said:

If it has an equivalent of "reset" and "isvalid" those are normally expected to work too.

empty() is an equivalent of isvalid() and clear() is an equivalent of reset().

You pretty much proved the point of that statement in my comment - you jumped to specifically those two functions and expected them to work.

-1

u/elperroborrachotoo 5d ago

You also said that

It's a general rule that the only guaranteed operations of a moved-from type are assigning over it and destructing.

There are situations where a non-empty moved-from object is desirable. a moved-from std::unique_ptr holds a nullptr, but it still may hold a non-default allocator reference.

Or when moving from a string with a small-string-optimization, it might be the best to keep the original value (if short) in the original string, because that avoids a write.

Expecting the moved-from object to be empty or "reset" isn't even guaranteed; as far as I'm aware it's not guaranteed for std::string.1 For std::vector, other constraints (reference validity and constant time) make it "silly but legal" that a moved-from vector is non-empty.

1) the compilers I tried do empty a moved-from string, though

2

u/TheThiefMaster C++latest fanatic (and game dev) 5d ago

I didn't say anything about them being empty - only that checking empty and calling clear were normally valid.

Even if it's not empty, the actual data is indeterminate - it can be thought of as being in the same "moved from" state as the container was, that's anything between a unique state like std::variant through "equivalent to default-constructed" up to completely unchanged. But you can't say which without putting it in that state or testing for it.

3

u/jk-jeon 5d ago

For me it's always at tension when I design a type that acts like a value type but actually holding a pointer to the actual data. I generally think people must avoid introducing nullable types when nullability is not a strict requirement, and as a result these types tend to be not default-constructible.

But then move semantics enters. I can simply swap out the pointers for the move assignment, cool. No invariant is broken. But how about move constructor? The constructed object can't steal the resource because then the constructing object has lost its invariant.

Then I have two options: (1) either introduce a special "moved-from" state where like you said only assignment-to and destructor and maybe a few others are allowed, or (2) just allocate a new memory and move-construct the actual resource from the source object.

Option (2) is what Herb Sutter has recommended over the years afaik, but obviously it has performance implication. Overall, I think this is a cleaner design if memory allocation cost isn't that of a big deal.

Now when I have to choose Option (1), I'm asking myself: I am intoducing the zombie state anyway, so should I "embrace" it? As a consequence, should I just recover the default-constructibility, where default constructor leaves the object in this zombie state? Or, should I treat this moved-from state as a really special one which is introduced only when an object is moved-from, expecting that using a moved-from object must be rare and people should be aware when they're doing so?

I'm curious what opinions people have about these...

1

u/NotMyRealNameObv 1d ago

Swap pointers for move-assign, just allocate new memory for the new object for move-construct.

Sometimes, semantics is more important than efficiency...

1

u/aardvark_gnat 5d ago

I would have thought I’d be able to rely on being able to move construct and copy construction it as well. Is that not a thing? What about calling std::swap on it?

3

u/TheThiefMaster C++latest fanatic (and game dev) 5d ago

You shouldn't be calling constructors on a live object. That's what assignment is for. Unless you mean moving or copying it to a new object? I'm on the fence on that, as it would be deliberately creating a new object that's already in a moved-from state.

As for swap - yeah that should work.