c++language-lawyerundefined-behaviorlifetimeiso

In the C++ standard, paragraph 6.7.3.5, what does "depends on the side effects" mean?


The paragraph in question:

A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling a destructor or pseudo-destructor (7.5.4.4) for the object. For an object of a class type, 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 (7.6.2.9) is not used to release the storage, the destructor is not implicitly called and any program that depends on the side effects produced by the destructor has undefined behavior.

What does this mean?

I am confused by the last sentence. It has a natural "if X then Y" form, with Y being a fairly serious property of undefined behaviour, but the X is not clear to me. Two interpretations seem reasonable to me (for now):

  1. The observable side-effects should not change with the lack of the destructor call. (thinking along the lines of the "as-if" rule)
  2. If other code requires the assumption that the destructor gets called in proof of well-formedness, then not calling the destructor is undefined behaviour

Regarding the 1st possibility:
Consider the following code: https://godbolt.org/z/sb3jEsePq
In line 30 we force the evaluation of f() during compile time, in a constant-evaluated context
In line 26 we end the lifetime of the object at &s storage without calling the destructor, and immediately start the lifetime of another object
Considering the static_assert doesn't fail, this means that the compiler didn't consider f() to invoke undefined behaviour
The behaviour is the same with both clang and gcc
Note that the first object, had it been destructed, would've had observable side-effects (printing to stdout)
In main() we have the same code but executed during runtime instead of compile time, to see if the undefined behaviour sanitizer catches anything
The fact that the compiler doesn't consider this undefined behaviour implies to me that it isn't undefined behaviour, or the compiler is wrong
If it isn't undefined behaviour, it feels like evidence against the 1st possibility
Also, it feels like this would be diagnosable, the compiler could try to evaluate the destructor and see if it has observable side-effects

Regarding the 2nd possibility:
It feels redundant in a sense to mention it (not that that is a horrible thing).

I am currently leaning towards the 2nd explanation, but am not sure if it's right.

I could not find an explanation in the standard on what "depends" would mean here exactly (though I haven't gone through it fully). In normal speech, I would not consider this program to depend on the side effects. To me the word would be applicable to situations in which a crucial invariant for the correctness of the program is violated by not invoking the destructor.

Copy of the code from godbolt (compiler flags are -std=c++20 -O3 -fsanitize=undefined):

#include <new>
#include <iostream>

struct S {
private:
    bool m_print;
public:
    constexpr S() : m_print(false){}

    S(const S&) = delete;
    S(S&&) = delete;

    S& operator=(const S&) = delete;
    S& operator=(S&&) = delete;

    constexpr void print_buffered(){m_print = true;}

    constexpr ~S(){
        if (m_print) std::cout << "HELLO WORLD" << std::endl;
    }
};

constexpr int f(){
    S s;
    s.print_buffered();
    std::construct_at(&s); // placement new not usable in constant-evaluated contexts :'(
    return 12;
}

static_assert(f() == 12);

int main(){
    std::cout << "Hi\n";
    S s;
    s.print_buffered();
    new (&s) S; // this ends lifetime of object and starts lifetime of new object
    std::construct_at(&s); // ^ same
}

As mentioned in previous section, I tried forcing a constant-evaluated context to see if compilers consider such situations as UB

The reference number of the quoted standard is: ISO/IEC 14882:2020(E)


Solution

  • It was never clear to me either what this half-sentence is supposed to mean and apparently I wasn't the only one, since it has been removed for C++23 via resolution of CWG issue 2523 with the discussion of the issue basically describing your two possible interpretations as well.

    This was also accepted as a defect report against previous versions of the standard.