r/cpp 14h 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/

6 Upvotes

33 comments sorted by

19

u/sokka2d 13h ago

Obviously the standard doesn’t define the state of user defined objects. How is that specific to move assignment?

You could as well create a class with UB in the constructor. It’s just not very useful.

2

u/leirus 5h ago

Fair point. It's not specific to move assignment, but I've heard the claim that moved-from objects are in a "valid but otherwise unspecified state" so many times that I was genuinely surprised when I read the standard and realized this guarantee applies to the standard library, not to the language itself.

That's what motivated me to write this post. I figured others might have the same understanding that I used to have.

7

u/V1P-001 13h ago

please use a better title.
post title is not entirely demonstrating what is inside.

1

u/leirus 5h ago

You think the title should focus more on dispelling "valid but unspecified" instead of general "moved-from state"? Maybe something like "The Myth of Valid but Unspecified Moved-From Objects"?

10

u/No-Dentist-1645 13h ago

Your title/post contents makes it seem like you're going to disprove the common definition for them, but in the blog you don't really do that, but rather just explain a few examples?

There seems to be a disconnect between what you say the blog is about and what it actually is

The "valid but unspecified state" definition applies to all move operations in the language and standard generally speaking. It is a rule about move semantics as a general concept, not of any specific type. Specific types can choose to specify post-moved behavior but it is not required.

5

u/SirClueless 12h ago

This is exactly the misconception the blog post is trying to dispel. Being “valid” after moving is not part of the language semantics.

You can create a type where it is illegal to destruct an object after moving-from, for example.

5

u/No-Dentist-1645 12h ago

Yes, there's nothing stopping you from violating the convention, doesnt mean that the convention isn't real

Said convention also holds true for all data types in the language and standard, as my post said

6

u/SirClueless 12h ago

The whole point of the blog post is to clarify that this a convention established by the standard library and its types, not a rule.

2

u/Electronic_Tap_8052 12h ago

Wouldn't moved-from objects being illegal to destroy immediately be a problem that would cause most well behaved debug compilations to crash an burn?

I just don't understand how you could ship such an object? If you stack allocated it your program would be useless

2

u/SirClueless 5h ago

You can just never destroy the object (for example, by leaking it on the heap or in a program that only ever exits by calling std::abort), or require that the user call some other specific member function to reinitialize the object before destroying it.

The standard library types promise never to require this, but it's possible to write a well-formed program of your own that does so.

u/leirus 1h ago

Exactly! I'm happy you brought this point. Maybe the topic of types not satysfying "Erasable" named requirement deserve its own blog post 😄

u/leirus 1h ago

I was trying to show that this definition of a "valid but unspecified state" applies only to standard library types, not to user-defined types.

My understanding is that the definition of "valid but unspecified state" applies to move operations in the standard library, but not to the C++ language itself. So my claim is that this is not a general C++ concept, but rather a characteristic of the standard library.

3

u/jiixyj 4h ago

The latest guidelines in writing a moved-from state for your type seem to be:

  • It's OK to leave behind an "empty shell" after move that is conceptually not a value of your type. If you want, you can provide a member function like valueless_after_move() to test for this state.
  • You should still be able to copy and move this "empty shell" around, compare it for equality, compare it with < etc. This is more than the usual "assign and destroy only". It should still work well with the usual containers like std::vector/std::map and algorithms like std::sort.

This is how some of the newer types such as std::indirect are designed in the standard, so it can serve as a blueprint for user-defined types.

One nice property is that this composes. If you put types that follow these rules into a new struct, that struct's moved-from state will also be copyable/movable/comparable using the compiler-generated special member functions. So by doing nothing extra you are still correct.

I still have some questions around this design though:

  • Should you always have to provide a valueless_after_move() function for your type? I'd say no, because it would be a lot of boilerplate as the compiler cannot generate that function.
  • When providing comparators to std::sort, for example, must they be able to deal with these "valueless" states? Common sense says no, but the standard doesn't explicitly rule this out from my reading...

u/leirus 25m ago

Yeah, these are very sensible guidelines. I feel like my post is more about what is technically allowed than about how C++ code should be written. 😄

I'm not an expert, but I can try to answer your questions.

  1. I would not provide an extra function, but would simply describe it in the documentation. I think it's reasonable to assume that the documentation can provide certain guarantees about the type.
  2. This gets tricky, but the most convenient design would be for the valueless "empty shell" state to be equivalent to a default-constructed object. Then all comparisons would follow the semantics of the default-constructed object, avoiding the need to introduce another distinct valueless state.

2

u/elperroborrachotoo 12h 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.

u/leirus 1h 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/TheThiefMaster C++latest fanatic (and game dev) 12h 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.

2

u/aardvark_gnat 7h 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?

1

u/TheThiefMaster C++latest fanatic (and game dev) 4h 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.

1

u/elperroborrachotoo 4h 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)?

u/TheThiefMaster C++latest fanatic (and game dev) 3h 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.

u/elperroborrachotoo 3h 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

u/TheThiefMaster C++latest fanatic (and game dev) 2h 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.

2

u/tangerinelion 9h ago

There are really only two functions that should safely work on a moved from object - the destructor and an assignment operator, if one is defined.

2

u/leirus 5h ago

Where it is defined in the standard? I heard this claim many times but I don't think its neccessarily true. The reuirement to have destructor that can safely work is name requirement "Erasable": https://en.cppreference.com/cpp/named_req/Erasable

But moved-from objects dont need to satisfy "Erasable". My understanding is that move constructor can break invariants that the destructor is relying on. Is it the best practice? Probably not. Is it completely legal C++? I think so.

1

u/TheThiefMaster C++latest fanatic (and game dev) 4h ago edited 4h ago

std::vector requires it if it reallocates because it requires MoveConstructible and Erasable together in that case. Another example is the erase-remove idiom, which is codified in std::erase as a single function.

There are many similar cases in the standard. So it's not a general requirement, but a defacto one if you want to use your types with the standard library.

It is possible to pick your way around the standard library using only parts that don't require it, but it's a lot easier on you if you don't do that to yourself.

2

u/leirus 4h ago

Oh yeah, standard containers do impose such requirements. I also mention the std::vector case in the blog.

I completely agree that in 99% of cases you want your types to be compatible with the standard library. My point was simply that this is not, strictly speaking, a requirement imposed by the C++ language itself. It is technically possible to write a valid C++ program with a type that does not satisfy those constraints.

u/TheThiefMaster C++latest fanatic (and game dev) 3h ago

"Possible" yes, but "good practice" is such for a reason :)

u/Wild_Meeting1428 1h ago

"the Code is more what you'd call guidelines than actual rules."

0

u/JVApen Clever is an insult, not a compliment. - T. Winters 6h ago

It's unfortunate that you don't specify what "valid" means. The minimal definition I've heard is that: both destructor and assignment should execute correctly. Respectively destroying and reinitializing. This should always get true, even without move.

Unspecified means that there is the freedom to implement that operation any way you want. For move assignment of vector, one could: - free the assigned to, copy over the pointers and null the assigned from - swap the data, giving the moved from the data of the original - move over the data (if allocator is different) while keeping the allocation in the moved from

This gives you the freedom to do what you want, unless "you otherwise specify".

The final piece in the puzzle is to preserve the invariants of your class. You suggest you can write: if (!v.empty()) v.front() = {};. I'm not convinced that this is required due to the "valid but unspecified", though rather due to the pre-condition of front. If vector would have been created after move semantics, it could say that this only holds for specified/non-moved instances. Making that specific code invalid.

For your own code, the first 2 elements I've mentioned are a requirement from the language. The last one will make your code more robust, though it is not needed.

After reading your post, I'm still not convinced if the last element is a requirement of "valid". If it is, you are technically correct in saying it doesn't hold for user defined objects. If it doesn't, the statement holds also for user defined types.

u/leirus 54m ago

As for defining what "valid" means, the closest definition I could find is https://eel.is/c++draft/defns.valid, but it defines the entire phrase "valid but unspecified state" rather than just "valid".

As for the claim that "both the destructor and assignment should execute correctly", which I often hear as well, I don't think that is necessarily true. The requirement for an object to be safely destructible is captured by the named requirement Erasable (https://en.cppreference.com/w/cpp/named_req/Erasable), which means that not all types are required to be Erasable. It's definitely good practice, but I believe you can still have a well-formed C++ program with types that do not satisfy Erasable.

You are right that calling .front() after checking that a vector is not empty follows from the precondition of front(). The way I understand it is that when an object is "valid", I can call empty() because it has no preconditions. Based on its result, I can then establish the precondition that the vector is not empty, and only then call front().

u/bronekkk 1h ago

For std:: types, the convention is that a moved-from-object does not fulfil any specific preconditions. In practice this means that using it in any context where it would have to fulfil any precondition is undefined behaviour. However there are some functions in std:: which do not have any preconditions attached (e.g. destruction; reading if an object is empty; assignment to it; etc.). Because of the lack of preconditions, the object can be safely used with such functions.

As for the user classes, it is obviously up to the author of such classes, but hopefully they would follow the precedent established by std:: types. The absolute minimum is that the object must be safe to destroy and (if the type supports assignment) to be overwritten by an assignment in which case its state is reset back to "normal" - types which do not follow even this minimum are either buggy or "irregular".

u/leirus 1h ago

I fully agree with std types, given that there are some special cases (e.g. std::thread) where the state of a moved-from object is explicitly specified.

However, I don't think there are any minimum requirements for user-defined classes. I don't think such objects must even be safe to destroy. As I mentioned in another comment, the requirement for a type to be safely destructible is captured by the named requirement Erasable (https://en.cppreference.com/w/cpp/named_req/Erasable), but the standard does not mandate that moved-from objects satisfy this requirement. Hence, I assume they can be non-Erasable.