Hello Andrzej and all, On Wed, Nov 29, 2017 at 11:48 PM, Andrzej Krzemienski via Boost <boost@lists.boost.org> wrote:
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.
I agree. In code: #include <boost/contract.hpp> class myfile { void invarinat() const { if(is_valid()) BOOST_CONTRACT_ASSERT(handle_ != -1); // Else, only assertions the destructor absolutely needs to be true. } bool valid_; file_handle handle_; public: bool is_valid() const { boost::contract::check c = boost::contract::public_function(this); return valid_; } void read() { boost::contract::check c = boost::contract::public_function(this) .precondition([&] { BOOST_CONTRACT_ASSERT(is_valid()); }) ; /* ... */ } myfile& operator=(myfile&& from) { boost::contract::check c = boost::contract::public_function(this) .precondition([&] { BOOST_CONTRACT_ASSERT(from.is_valid()); }) .postcondition([&] { BOOST_CONTRACT_ASSERT(is_valid()); BOOST_CONTRACT_ASSERT(!from.is_valid()); }) ; /* ... */ } ~myfile() { boost::contract::check c = boost::contract::public_function(this); /* ... */ } /* ... */ }; Two questions: 1. Is it OK to assume I can call is_valid() on a moved-from object (so I can put it to guard invariants, preconditions, etc. and also check it in user code where needed to satisfy preconditions)? Based on the replies to this email thread so far, I think the answers is "yes". 2. How useful is a class like the one above with "crippled" invariants and is_valid() preconditions on all its useful public methods like read()? The answer seems to be: not very useful. I guess that's the price to pay for the performance gain of moving objects around... Thanks, --Lorenzo