Boost logo

Boost :

Subject: Re: [boost] [contract] move operations and class invariants
From: Lorenzo Caminiti (lorcaminiti_at_[hidden])
Date: 2017-12-01 01:52:22


Hello Andrzej and all,

On Wed, Nov 29, 2017 at 11:48 PM, Andrzej Krzemienski via Boost
<boost_at_[hidden]> 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


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk