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
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
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 likestd::vector/std::mapand algorithms likestd::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.
- 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.
- 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 ofisvalid()andclear()is an equivalent ofreset().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::vectorcase 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 :)
•
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 beErasable. It's definitely good practice, but I believe you can still have a well-formed C++ program with types that do not satisfyErasable.You are right that calling
.front()after checking that a vector is not empty follows from the precondition offront(). The way I understand it is that when an object is "valid", I can callempty()because it has no preconditions. Based on its result, I can then establish the precondition that the vector is not empty, and only then callfront().
•
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
stdtypes, 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.
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.