Title
Undefined behavior via omitted destructor call in constant expressions
Status
open
Section
7.7 [expr.const]
Submitter
Jiang An

Created on 2021-09-06.00:00:00 last changed 1 month ago

Messages

Date: 2022-04-19.05:57:55

Suggested resolution:

Change in 6.7.3 [basic.life] paragraph 5 as follows:

A program may end the lifetime of an object of class type without invoking the destructor, by reusing or releasing the storage as described above. [Note 3: A delete-expression (7.6.2.9 [expr.delete]) invokes the destructor prior to releasing the storage. —end note] In this case, the destructor is not implicitly invoked and any program that depends on the side effects produced by the destructor has undefined behavior. [Note: The correct behavior of a program often depends on the destructor being invoked for each object of class type. -- end note]
Date: 2022-04-15.00:00:00

Additional notes (April, 2022):

The phrase "[a] program that depends on the side effects" may have these meanings:

  • the program depends on the side effects if it would have undefined behavior if they don't happen (which they don't): this would mean the second half of the sentence has no normative effect and can be struck
  • the program depends on the side effects if it would have different observable behavior if (in violation of the standard) the destructor were actually invoked by the implementation when the object's lifetime ended
  • something else

The second option would need a fork in the evaluation of constant expressions to determine whether undefined behavior occurs.

Date: 2021-09-06.00:00:00

According to 7.7 [expr.const] bullet 5.8, one criterion that disqualifies an expression from being a core constant expression is:

an operation that would have undefined behavior as specified in Clause 4 [intro] through Clause 15 [cpp]

One potential source of undefined behavior is the omission of a call to a destructor for a constructed object, as described in 6.7.3 [basic.life] paragraph 5:

A program may end the lifetime of an object of class type without invoking the destructor, by reusing or releasing the storage as described above. [Note 3: A delete-expression (7.6.2.9 [expr.delete]) invokes the destructor prior to releasing the storage. —end note] In this case, the destructor is not implicitly invoked and any program that depends on the side effects produced by the destructor has undefined behavior.

For example:

  #include <memory>

  constexpr int test_basic_life_p5() {
    class guard_t {
      int &ref_;
    public:
      explicit constexpr guard_t(int &i) : ref_{i} {}
      constexpr ~guard_t() { ref_ = 42; }
    };

    int result = 0;

    auto alloc = std::allocator<guard_t>{};
    auto pguard = alloc.allocate(1);
    std::construct_at(pguard, result);
    // std::destroy_at(pguard);
    alloc.deallocate(pguard, 1);

    return result;  // value depends on destructor execution
  }

  int main() {
    constexpr auto v = test_basic_life_p5();
    return v;
  }

It is not clear that it is reasonable to require implementations to diagnose this form of undefined behavior in constant expressions.

A somewhat related question is raised by the restrictions on the use of longjmp in 17.13.3 [csetjmp.syn] paragraph 2:

A setjmp/longjmp call pair has undefined behavior if replacing the setjmp and longjmp by catch and throw would invoke any non-trivial destructors for any objects with automatic storage duration.

Here the undefined behavior occurs for any non-trivial destructor that is skipped, not just one for which the program depends on its side effects, as in 6.7.3 [basic.life] paragraph 5. Perhaps these two specifications should be harmonized.

History
Date User Action Args
2022-04-19 05:57:55adminsetmessages: + msg6804
2022-04-19 05:57:55adminsetmessages: + msg6803
2021-09-06 00:00:00admincreate