Short answer
No — under C++20, a constexpr new expression evaluated in a constant expression requires the complete object type to be trivially destructible. If it is not, the expression is ill-formed in a constant context, and a conforming compiler must reject it. Your local builds succeed because of a compiler extension, not because the standard is ambiguous on this point.
Separating confirmed rules from the apparent ambiguity
The confusion in your question comes from reading the lack of an explicit ban on non-trivial destructors as permission. The confirmed rules are:
- Non-trivial destructors: In C++20,
constexpr new T in a constant expression requires T to be trivially destructible. There is no sanctioned path for constexpr delete on non-trivially destructible types in C++20. - Timing of deletion: Deallocation must happen within the same constant expression evaluation. A constexpr allocation cannot be deferred to runtime, and it cannot be leaked — an allocation that outlives the constant evaluation makes the whole expression ill-formed.
- Exceptions: If construction throws during constant evaluation, no object lifetime begins, so no deallocation is required; the allocation is discarded. (Separately, an exception escaping during constant evaluation generally makes the evaluation fail anyway.)
Why your local builds pass
The likely explanation is implementation extensions. GCC and Clang have offered relaxed constexpr-allocation modes (e.g. -fconstexpr-allocations) that accept non-trivial types in constexpr contexts. That code is non-conforming C++20, which is exactly why stricter production toolchains reject it. This is version- and flag-sensitive, so treat any local success as a signal to check flags, not as evidence of standard support.
One caveat worth flagging for review: C++23 evolved the constexpr allocation rules and relaxes some restrictions when destructors are themselves constexpr. If your production target is C++23-capable, verify against that draft rather than assuming the C++20 rule still binds.
How to verify on your toolchain
A minimal probe:
struct T { ~T() {} }; // non-trivial destructor
constexpr bool probe() {
T* p = new T;
delete p;
return true;
}
static_assert(probe());
Compile with -std=c++20 -pedantic-errors (GCC/Clang) or /std:c++20 /permissive- (MSVC). A conforming compiler rejects the static_assert. Then recompile with the extension flag explicitly disabled to confirm your local success depends on it.
Practical recommendation
For portable C++20 code, restrict constexpr allocation to trivially destructible types, or wrap non-trivial state behind a trivially destructible handle. If you genuinely need non-trivial destruction at compile time, that is a C++23 conversation — confirm your production compiler's support level before relying on it.