This is an unofficial snapshot of the ISO/IEC JTC1 SC22 WG21 Core Issues List revision 116a. See http://www.open-std.org/jtc1/sc22/wg21/ for the official list.

2024-12-19


2523. Undefined behavior via omitted destructor call in constant expressions

Section: 7.7  [expr.const]     Status: C++23     Submitter: Jiang An     Date: 2021-09-06

[Accepted as a DR at the February, 2023 meeting.]

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.4 [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.4 [basic.life] paragraph 5. Perhaps these two specifications should be harmonized.

Additional notes (April, 2022):

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

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

Proposed resolution (approved by CWG 2022-11-11):

Change in 6.7.4 [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]