For this answer, I will be using a 2012 C++11 release of the C++ standard, which can be found here (C++ standard), because this is freely available and up to date.
The following three terms used in your question occur as followed:
- Destructor - 385 times
- Side effect - 71 times
- Depends - 41 times
Sadly "depends on the side effect" appears only once, and DEPENDS ON is not an RFC standardized identifier like SHALL, so it's rather hard to pin down what depends means.
Depends on
Let's take an "activist judge" approach, and assume that "depends", "dependency", and "depending" are all used in a similar context in this document, that is, that the language was used to convey a broad idea rather than to convey a legalease concept.
Then we can analyze this portion of page 1194:
17.6.3.2
Effect on original feature: Function swap moved to a different header
Rationale: Remove dependency on for swap.
Effect on original feature: Valid C++ 2003 code that has been compiled expecting
swap to be in < algorithm > may have to instead include < utility >.
This portion indicates a strict sort of dependency; you originally needed to include to get std::swap. "depends on" therefore indicated a strict requirement, a necessity so to speak, in the sense that there is not sufficient context without the requirement to proceed; failure will occur without the dependency.
I chose this passage because it conveys the intended meaning as clearly as possible; other passages are more verbose, but they all include a similar meaning: necessity.
Therefore, a "depends on" relationship means that the thing being depended on is required for the depending item to make sense, be whole and complete, and be usable in a context.
To cut through that legalese red tape, this means A depends on B means A requires B. This is basically what you'd understand "depend" to mean if you looked it up in a dictionary or spoke it in a sentence.
Side effect
This is more strictly defined, on page 10:
Accessing an object designated by a volatile glvalue (3.10), modifying an object, calling a library I/O
function, or calling a function that does any of those operations are all side effects, which are changes in the
state of the execution environment.
This means that anything which results in a change to the environment (such as RAM, network IO, variables, etc etc) are side effects. This neatly fits with the notion of impurity/purity from functional languages, which is clearly what was intended. Note that the C++ standard does not require that such side effects be observable; modifying a variable in any way, even if that variable is never looked at, is still a side effect.
However, due to the "as if" rule, such unobservable side effects may be removed, page 8:
A conforming implementation executing a well-formed program shall produce the same observable behavior
as one of the possible executions of the corresponding instance of the abstract machine with the same program
and the same input. However, if any such execution contains an undefined operation, this International
Standard places no requirement on the implementation executing that program with that input (not even
with regard to operations preceding the first undefined operation).
Depends on the side effects
Putting these two definitions together, we can now define this phrase: something depends on the side effects when those changes to the execution environment are required in order to satisfy the senseful, whole, and complete operations of the program. If, without the side effects, some constraint is not satisfied that is required for the program to operate in a standard compliant way, we can say that it depends on the side effects.
A simple example to illustrate this would be, as stated in another answer, a lock. A program that uses locks depends on the side effect of the lock, notably, the side effect of providing a serialized access pattern to some resource (simplified). If this side effect is violated, the constraints of the program are violated, and thus the program cannot be thought of as senseful (since race conditions or other hazards may occur).
The program DEPENDS on the constraints that a lock provides, via side effects; violating those results in a program that is invalid.
Depends on the side effects produced by the destructor
Changing the language from referring to a lock to a destructor is simple and obvious; if the destructor has side effects which satisfy some constraint that is required by the program to be senseful, whole, complete, and usable, then it depends on the side effects produced by the destructor. This is not exactly difficult to understand, and follows quite readily from both a legalese interpretation of the standard and a cursory layman understanding of the words and how they are used.
Now we can get into answering your questions:
Under which condition(s), if any, does this program exhibit Undefined Behavior?
Any time a dependency or requirement is not fulfilled because a destructor is not called, the behavior of any dependent code is undefined. But what does this really mean?
1.3.24
undefined behavior
behavior for which this International Standard imposes no requirements
[ Note: Undefined behavior may be expected when this International Standard omits any explicit definition of
behavior or when a program uses an erroneous construct or erroneous data.
Permissible undefined behavior
ranges from ignoring the situation completely with unpredictable results, to behaving during translation or
program execution in a documented manner characteristic of the environment (with or without the issuance of
a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message).
Many erroneous program constructs do not engender undefined behavior; they are required to be diagnosed.
— end note ]
Let's suppose for a moment that such behavior WAS defined.
Suppose it was explicitly illegal. This would then require any standard compiler to detect this case, to diagnose it, to deal with it in some fashion. For example, any object not explicitly deleted would have to be deleted at program exit, requiring some sort of tracking mechanism and ability to issue destructors to arbitrary types, possibly not known at compile time. This is basically a garbage collector, but given it's possibly hide pointers, it's possible to call malloc, etc etc, it would be essentially infeasible to require this.
Suppose it was explicitly allowed. This would also allow compilers to remove destructor calls, under the as-if rule, since hey, you can't depend on that behavior anyway. This would result in some nasty surprises, mostly related to memory not freeing very quickly or easily. To get around that, we'd all start using finalizers, and the problem arises yet again. Furthermore, allowing that behavior means that no library can be sure when their memory is recovered or if it ever will be, or if their locks, OS dependent resources, etc etc, will ever get returned. This pushes the requirements for clean up from the code using the resources to the code providing it, where it's basically impossible to deal with in a language like C or C++.
Suppose it had a specific behavior; what behavior would this be? Any such behavior would have to be quite involved or it wouldn't cover the large number of cases. We've already covered two, and the idea of cleaning up for any given object at program exit imposes a large overhead. For a language meant to be fast or at least minimal, this is clearly an unnecessary burden.
So instead, the behavior was labeled undefined, meaning any implementation is free to provide diagnostics, but also free to simply ignore the problem and leave it to you to figure out. But no matter what, if you depend on those constraints being satisfied but fail to call the destructor, you are getting undefined behavior. Even if the program works perfectly well, that behavior is undefined; it may throw an error message in some new version of Clang, it may delete your hard drive in some incredibly secure cryptographic OS of the far flung future, it may work until the end of time.
But it's still undefined.
Your Example
Your example does not satisfy the "depends on" clause; no constraint that is required for the program to run is unsatisfied.
- Constructor requires a well formed pointer to a real variable: satisfied
- new requires a properly allocated buffer: satisfied
- printf requires an accessible variable, interpretable as an integer: satisfied
No where in this program does a certain value for x or a lack of that value result in a constraint being dissatisfied; you are not invoking undefined behavior. Nothing "depends" on these side effects; if you were to add a test which functioned as a constraint that required a certain value for "x", then it would be undefined behavior.
As it stands, your example is not undefined behavior; it's merely wrong.
Finally!
Is forgetting to call a destructor any different than forgetting to call an ordinary function with the same body?
It is impossible in many cases to define an ordinary function with the same body:
- A destructor is a member, not an ordinary function
- A function cannot access private or protected values
- A function cannot be required to be called upon destruction
- A finalizer also cannot be required to be called upon destruction
- An ordinary function cannot restore the memory to the OS without calling the destructor
And no, calling free on an allocated object cannot restore the memory; free/malloc need not work on things allocated with new, and without calling the destructor, the private data members will not be released, resulting in a memory leak.
Furthermore, forgetting to call a function will not result in undefined behavior if your program depends on the side effects it imposes; those side effects will simply not be imposed, and your program will not satisfy those constraints, and probably not work as intended. Forgetting to call a destructor, however, results in undefined behavior, as stated on page 66:
For an object of a class type
with a non-trivial destructor, the program is not required to call the destructor explicitly before the storage
which the object occupies is reused or released; however, if there is no explicit call to the destructor or if a
delete-expression (5.3.5) is not used to release the storage, the destructor shall not be implicitly called and
any program that depends on the side effects produced by the destructor has undefined behavior.
As you referenced in your original question. I don't see why you had to ask the question, given you already referenced it, but there you go.