Consteval destructors

Document number: P3421R0
Date: 2024-10-12
Reply-to: Ben Craig <ben dot craig at gmail dot com>
Audience: Evolution Working Group, SG7 Compile-time programming

Changes from previous revisions

R0

First revision!

Introduction

This proposal makes it possible to mark destructors as consteval. Prior to this proposal, destructors could already be marked constexpr. Programs that attempt to invoke a consteval destructor outside of constant evaluation are ill-formed.

Motivation and Scope

Reflection and freestanding

This proposal is largely motivated by P3295 Freestanding constexpr containers and constexpr exception types, which is in turn motivated by P2996 reflection in freestanding environments. A consteval std::vector used during reflection computations would be much better if the destructor were consteval rather than constexpr. When the destructor is constexpr, a default constructed (i.e. empty) constexpr std::vector's destructor will still execute at runtime, eventually invoking operator delete. If the destructor is constevel, then such a case will be ill-formed, which will properly encourage the developer to refactor the code, likely into a consteval function.


When this proposal is accepted, P3295 Freestanding constexpr containers and constexpr exception types will be updated to make string, vector, and the <stdexcept> exceptions' destructors freestanding-consteval.

// current
struct current_str {
    char *buf = nullptr;
    consteval current_str() = default;
    // allocating constructors omitted for this example

    constexpr ~current_str() { // "only" constexpr
        delete[] buf;
    }
};

void test_current() {
    current_str s; // compiles and emits a delete[] call!
}

// proposed
struct proposed_str {
    char *buf = nullptr;
    consteval proposed_str() = default;
    // allocating constructors omitted for this example

    consteval ~proposed_str() { // now consteval
        delete[] buf;
    }
};

void test_future() {
    proposed_str s0; // ill formed!
    const proposed_str s1; // fine
    constexpr proposed_str s2; // fine
}

Teachability for compile-time types

There are teachability benefits to consteval destructors as well. If I want to make a class that only exists at compile-time, I may be tempted to mark every function as consteval. This runs into the direct problem of destructors. If the destructor is "only" marked constexpr, then there's the indirect problem of functions called by destructors. In many cases, it is not allowable for a constexpr function to call a consteval function. The end result is that users need to jump through hoops to end up with a class that can still end up accidentally leaking into runtime.

struct my_str {
    char *buf = nullptr;
    consteval void clear() {
        delete[] buf;
        buf = nullptr;
    }
    // constexpr can't call consteval here
    // constexpr ~my_str() {
    //     clear();
    // }
    consteval ~my_str() { // proposed
        clear();
    }
};

Design Considerations

Non-transient constexpr allocations

There is a desire from some in the committee to allow allocating constant evaluated vectors and strings persist into runtime. This is referred to as non-transient constexpr allocations. This paper doesn't propose such facilities, but the paper is trying to avoid causing issues for those future papers.

With this proposal, a class with consteval constructors and consteval destructors can be used at runtime, so long as an allocation doesn't cross the boundary. I would expect future non-transient constexpr papers to extend this capability to allocating constructors in the future.

struct boundary_str {
    char *buf = nullptr;
    void opaque() const;
    consteval ~boundary_str() {
        delete[] buf;
    }
};

void test_opaque() {
    const boundary_str s;
    s.opaque(); // executes at runtime!
}

Implementation Experience

This paper was prototyped on clang. The implementation mainly needed to remove the old diagnostic that prevented consteval from being placed on destructors, then adding a check in the CodeGen portion of clang to error if a consteval destructor would be emitted.

In addition, the change was used in the MSSTL (compiled with the prototype clang) to ensure that making vector's and string's destructors consteval in freestanding would produce the desired results and diagnostics.

Wording

This paper's wording is based on the current working draft N4988.

Feature test macro

In [cpp.predefined], add a new macro to Table 22: Feature-test macros [tab:cpp.predefined.ft]:
__cpp_consteval_dtors 20????L

Changes in [expr.const]

Modify [expr.const]#19
An immediate function is a function, destructor, or constructor that is
  • declared with the consteval specifier, or
  • an immediate-escalating function F whose function body contains an immediate-escalating expression E such that E's innermost enclosing non-block scope is F's function parameter scope.
    [Note 11: 
    Default member initializers used to initialize a base or member subobject ([class.base.init]) are considered to be part of the function body ([dcl.fct.def.general]).
    — end note]

Changes in [dcl.constexpr]

Modify [dcl.constexpr]#2
A constexpr or consteval specifier used in the declaration of a function declares that function to be a constexpr function.
[Note 3: 
A function, destructor, or constructor declared with the consteval specifier is an immediate function ([expr.const]).
— end note]
A destructor, aAn allocation function, or a deallocation function shall not be declared with the consteval specifier.

Disclaimer

The opinions in this paper are my own and do not reflect the views or positions of my employer, CrowdStrike, Inc.