Boost logo

Boost :

Subject: Re: [boost] [contract] move operations and class invariants
From: Andrzej Krzemienski (akrzemi1_at_[hidden])
Date: 2017-11-30 07:48:54


2017-11-29 23:45 GMT+01:00 Gavin Lambert via Boost <boost_at_[hidden]>:

> 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;


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