Note: I\'ve seen similar questions, but none of the answers are precise enough, so I\'m asking this myself.
Whether a program "depends on the side effects produced by a destructor" hinges on the definition of "observable behavior".
To quote the standard (section 1.9.8, Program execution, bold face is added):
The least requirements on a conforming implementation are:
- Access to volatile objects are evaluated strictly according to the rules of the abstract machine.
- At program termination, all data written into files shall be identical to one of the possible results that execution of the program according to the abstract semantics would have produced.
- The input and output dynamics of interactive devices shall take place in such a fashion that prompting output is actually delivered before a program waits for input. What constitutes an interactive device is implementation-defined.
These collectively are referred to as the observable behavior of the program. [ Note: More stringent correspondences between abstract and actual semantics may be defined by each implementation. ]
As for your other question:
Is forgetting to call a destructor any different than forgetting to call an ordinary function with the same body?
Yes! Forgetting an "equivalent" call to a function leads to well defined behavior (whatever it was supposed to make happen doesn't happen), but it's quite different for a destructor. In essence, the the standard is saying that if you engineer your program such that an observable destructor is "forgotten," then you're no longer writing C++, and your program result is completely undefined.
Edit: Oh right, the last question:
Under which condition(s), if any, does this program exhibit Undefined Behavior?
I believe printf qualifies as writing to a file, and is therefore observable. Of course rand() is not actually random, but is completely deterministic for any given seed, so the program as written does exhibit undefined behavior (that said, I would be really surprised if it didn't operate exactly as written, it just doesn't have to).
My reading of this portion of the standard is:
Side effects here are simply changes in program state that result from calling the destructor. They will be things like updating reference counts, releasing locks, closing handles, stuff like that.
'Depends on the side effects' means that another part of the program expects the reference count to be maintained correctly, locks to be released, handles closed and so on. If you make a practice of not calling destructors, you need to make sure your program logic does not depend on them having been called.
Although 'forgetting' is not really relevant, the answer is no, destructors are just functions. The key difference is that under some circumstances they get called by the compiler ('implicitly') and this section of the standard defines a situation in which they will not.
Your example does not really 'depend on the side effects'. It obviously calls the random function exactly 3 times and prints whatever value it calculates. You could change it so:
Obviously, with this dependency the program would exhibit 'undefined behaviour' with respect to the reference count.
Please note that 'undefined behaviour' does not have to be bad behaviour. It simply means 'behavior for which this International Standard imposes no requirements'.
I really think there is a danger of overthinking what is fundamentally quite a simple concept. I can't quote any authority beyond the words that are here and the standard itself, which I find quite clear (but by all means tell me if I'm missing something).
It basically means that when you define your own destructor for a class, it is no longer called automatically upon leaving scope. The object will still be out of scope if you try to use it, but the memory will still be used up in the stack and anything in your non-default destructor will not happen. If you want the count of objects to decrease whenever you call your destructor, for example, it will not happen.
The standard is required to speak in such terms as observable behavior
and side effects
because, although many people often forget this, c++ is not just used for PC software.
Consider the example in your comment to Gene's answer:
class S {
unsigned char x;
public: ~S() {
++x;
}
};
the destructor here is clearly modifying an object -- hence that's a "side effect" with the given definition -- yet I'm pretty sure no program could "depend" on this side effect in any reasonable sense of the term. What am I missing?
you are missing the embedded world for example. Consider a bare metal c++ program running on a small processor with special function register access to a uart:
new (address_of_uart_tx_special_function_register) S;
here calling the destructor clearly has observable side effects. If we don't call it, the UART transmits one byte less.
Therefore whether side effects are observable also depends on what the hardware is doing with the writes to certain memory locations.
It may also be noteworthy that even if the body of a destructor is empty it could still have side effects if any of the classes member variables have destructors with side effects.
I don't see anything forbidding the compiler from doing other bookkeeping (maybe with regard to exceptions and stack unwinding). Even if no compiler currently does and no compiler ever will from a language lawyer point of view you still have to consider it UB unless you know that the compiler doesn't create side effects.
This exact issue with the wording is the subject of editorial pull request against the draft C++ standard [basic.life] Remove description of impossible UB which seeks to strike this wording from the draft standard:
A destructor that is not called cannot produce side effects, therefore it is impossible to depend on those side effects.
After much discussion it seemed to lean towards that direction:
Editorial meeting: The standard rules cannot depend on the programmer's intent. Pass to CWG with the intent of applying as-is.
but it needs to be reviewed by the Core Working Group (CWG) first and is therefore not an editorial change. I believe this means it will eventually show up as a defect report.
So in conclusion this looks like an open issue as to whether that wording has any meaning at all but it will be reviewied by CWG eventually.
This is indeed not a very well defined thing in the standard, but I would interpret "depends on" as meaning "the behavior under the rules of the abstract machine is affected".
This behavior consists of the sequence of reads and writes to volatile variables and the calls to library I/O functions (which includes at least the I/O functions of the standard library like printf
, but may also include any number of additional functions in any given implementation, e.g. WinAPI functions). See 1.9/9 for the exact wording.
So the behavior is undefined if execution of the destructor or lack thereof affects this behavior. In your example, whether the destructor is executed or not affects the value of x
, but that store is dead anyway, since the next constructor call overwrites it, so the compiler could actually optimize it away (and chances are, it will). But more importantly, the call to rand()
affects the internal state of the RNG, which influences the values returned by rand()
in the other object's constructor and destructor, so it does affect the final value of x
. It's "random" (pseudo-random) either way, but it would be a different value. Then you print x
, turning that modification into observable behavior, thus making the program undefined.
If you never did anything observable with x
or the RNG state, the observable behavior would be unchanged independent of whether the destructor is called or not, so it wouldn't be undefined.