2017-11-29 23:45 GMT+01:00 Gavin Lambert via Boost
On 30/11/2017 03:15, Peter Dimov wrote:
Lorenzo Caminiti wrote:
C++ requires that moved-from objects can still be destructed. Because
contract programming requires class invariants to hold at destructor entry, it follows that moved-from objects must still satisfy class invariants.
No, moved-from objects must be valid objects. Not only can they be destroyed, they must be usable. Their state is unspecified, but invariants hold.
In general you should expect to be able to call any method which is valid on a default-constructed object, *especially* assignment operators (as it's relatively common to reassign a moved-from object). (You cannot, however, actually assume that it will return the same answers as a default-constructed object would.)
Agreed (assuming you meant "on a moved-from-object" rather than "on a default-constructed object"), but while such an object is "valid", this information is of little use in some cases. And I think it is such cases that are relevant for creating class invariants. Let me give you some context. I would like to create a RAII-like class representing a session with an open file. When I disable all moves and copies and the default constructor (so that it is a guard-like object) I can provide a very useful guarantee: When you have an object of type `File` within its lifetime, it means the file is open and you can write to it, or read from it. This means calling `file.write()` and `file.read()` is *always* valid and always performs the desired IO operation. When it comes to expressing invariant, I can say: ``` bool invariant() const { this->_file_handle != -1; } ``` (assuming that -1 represents "not-a-handle") But my type is not moveable. So I add move operations (and not necessarily the default constructor), but now I have this moved-from state, so my guarantee ("When you have an object of type `File` within its lifetime, it means the file is open and you can write to it, or read from it") is no longer there. You may have an object to which it is invalid to write. Of course, the moved-from-object is still "valid", but now "valid" only means "you can call function `is_valid()` and then decide" (and of course you can destroy, assign, but that's not the point). Now, in turn, every function like `read()` or `write()` has a precondition: `is_valid()`. So object is always "valid" but calling 90% of its interface is invalid (unless you guarantee the precondition manually). The invariant informally is "either in a moved-from-state or you can use write/read", and there may be no way to express it in the code. This is still an "invariant", but it is *weak*, that is, it is less useful in practice. The previous invariant (in the guard-like design) is *strong* it has practical value to the user: I do not have to check anything before calling `read()`. The new invariant is *weak*: you have to "check" something time and again, and the design is more prone to bugs: you can call functions out of contract. The distinction into "weak" and "strong" invariants is not strict or formal, but it does matter in practice. I think this is the problem that Lorenzo is facing. Regards, &rzej;