wt., 16 kwi 2019 o 18:09 Peter Dimov via Boost
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;