Boost logo

Boost :

Subject: Re: [boost] [variant2] Formal review
From: Andrzej Krzemienski (akrzemi1_at_[hidden])
Date: 2019-04-17 08:33:17


wt., 16 kwi 2019 o 18:09 Peter Dimov via Boost <boost_at_[hidden]>
napisał(a):

> Andrzej Krzemienski wrote:
>
> > Let's introduce a new term: "effective invariant": this is a constraint
> on
> > object's state much as "invariant". It determines what values an object
> > can assume in a program where programmers adhere to the important
> > principles that are necessary for programs to be correct. We can list
> some
> > of them:
> >
> > * Destructors do not throw exceptions, even if they fail to release
> > resources
> >
> > * Objects that threw from the operation with basic exception safety,
> which
> > does not guarantee any other special behavior on exception, are never
> > read: they are either destroyed or reset,
> >
> > * Objects that are moved from, unless they explicitly guarantee
> something
> > more, are only destroyed or reset.
>
> "Effective invariants" have been tried before, and abandoned each time.
> This
> is basically the notion of "singular values", also known affectionately as
> "zombie objects". The most famous instance is probably two-phase
> construction, but signaling NaNs are another example. The idea is that
> these
> singular zombies "never occur" in the mythical correct program, so it's
> fine
> to make accesses to them undefined behavior.
>

What I hear you describe is types with weak invariants, where the zombie
state can be set very easily in the normal (not requiring special
attention) parts of the program:

```
float x = std::numeric_limits<float>::quiet_NaN();

TwoPhaseInitResource r {};
r.initialize("params");
use(r);
r.release_resources();
// risk: r could still be used here
```

I agree with you that this subject of easily obtained zombie state has been
explored, and I agree with you that types that expose it are dangerous and
had best be avoided. I would never want to promote in the slightest degree
a design like that.

However, you seem to be missing the distinction which I find very
important: a "zombie" state that can only occur in *very special
situations* (which correspond to the incorrect programs) is something
substantially different than "zombie" states that can occur everywhere. I
have never read or heard of "zombie states in very special circumstances"
having been explored. Therefore I consider the problem new and worth
exploring. If "zombie states in very special circumstances" have been
explored also and I am just ignorant of the work, I would welcome a
correction, ideally in form of a link.

Regarding the "never occur" part, I do not use the relation "`zombie states
in very special circumstances` only occur in incorrect programs" to claim
that "`zombie states in very special circumstances` never occur in
programs". Programs will have incorrect parts and they will likely result
in "zombie states in very special circumstances" occurring in the program.
However, in places where a program has a bug, it is better (and for sure:
not worse) to reflect this as UB, than to make the program appear correct
on the surface and have it do random things according to the well specified
rules of the abstract machine. This is because UB is a well understood
manifestation of a bug that tools like static analyzers can track and
report. Whereas superficial fixes that cover up user bugs are opaque to the
automated tools.

Let me give you one example. There are three ways to address the situation
where a two-phase-init Resource type can be used when it is not
initialized:

```
Resource r {}; // phirst phase init
use(r); // what to do?
r.initialize("params"); // second phase init
```

Option 1: We can call it UB: Put an assertion and/or explicit UB
declaration understood by the compiler (such as __builtin_unreachable(), or
__builtin_assume()).

Option 2: try to "fix" someone else's bug: if `r` does not contain a value,
initialize it on the fly with some invented initialization parameters and
go on making use of the now initialized resource. No UB, so some group of
people is satisfied with this solution.

Option 3: redesign the interface, so that the user is forced to change his
logic, and this change alone removes bugs. The redesign is to change a
two-phase-init interface to a RAII-like interface. Then making bugs is
impossible.

So, option 3 is superior; provided that it is implementable. If it is not
for some reason, option 1 is superior to 2, because option 2 conceals the
bug from tools and from code reviewers.

I treat the situation in the variant as such: unable to go with option 3,
so we should prefer option 1 to option 2.

> Typically, after a decade or so of experience, "never" is determined to
> occur much more frequently than previously thought, and the idea is
> abandoned, until its next discoverer.
>

I make a promise that if you convince me that "zombie states in very
special circumstances" are as bad as "zombie states everywhere" I will
document this, put it in my blog and somewhere in Boost docs, so that if
such discussion should be rehashed in the future there will be an easily
accessible link that people can be pointed to.

>
> There are two main problems with this school of thought: one, creating a
> dormant zombie object is the worst possible thing that can happen in a
> program, because the "undefined behavior" can occur much later in a
> context
> far removed from the one that caused the error. Two, abiding by the rules
> that govern the supposedly correct programs is too cumbersome and there's
> no
> enforcement and no immediate feedback (compile- or run time) when they are
> broken.
>

Again, I am not convinced that you are seeing the distinction between
"zombie states in very special circumstances" and "zombie states
everywhere". I agree with you on "zombie states everywhere".

Regards,
&rzej;


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