Search code examples
c++language-lawyerc++20constexprdelete-operator

Can class-specific deallocation functions be used in constant expressions?


Can I define one of class-specific deallocation functions and use it in a constant expression?

For example a destroying delete:

#include <new>

struct A {
    constexpr void operator delete(A *, std::destroying_delete_t);
};

struct B : A {};

constexpr void A::operator delete(A *p, std::destroying_delete_t) {
    ::delete static_cast<B*>(p);
}

constexpr bool foo() {
    A *p = new B;
    delete p;
    return true;
}

static_assert( foo() );

GCC and MSVC do accept it, however Clang complains:

error: constexpr function never produces a constant expression [-Winvalid-constexpr]
note: call to class-specific 'operator delete'

Online demo: https://godbolt.org/z/47h1Y4sr3

Which behavior is correct?


Solution

  • [expr.const]/5, bullet points 18-19 state that constant evaluation may not encounter:

    • a new-expression ([expr.new]), unless the selected allocation function is a replaceable global allocation function ([new.delete.single], [new.delete.array]) and the allocated storage is deallocated within the evaluation of E;
    • a delete-expression ([expr.delete]), unless it deallocates a region of storage allocated within the evaluation of E;

    This does not specify that the selected deallocation function must be a replaceable global deallocation function; as long as it ends up calling such a deallocation function if necessary to satisfy the preceding bullet point, your code is valid.

    A further possibly-motivating example:

    #include <new>
    struct A {
        int& r;
        constexpr void operator delete(A* p, std::destroying_delete_t) {
            p->r = 1;
            ::delete(p);
        }
    };
    constexpr int foo() {
        int i = 0;
        delete new A{i};
        return i;
    }
    static_assert(foo() == 1);
    

    An interesting question is raised by the following:

    #include <new>
    struct B {
        int** q;
        constexpr void operator delete(B* p, std::destroying_delete_t) {
            **p->q = 1;
            delete p->q;
        }
    };
    constexpr int bar() {
        int j = 0;
        B b{new int*(&j)};
        delete &b;
        return j;
    }
    static_assert(bar() == 1);
    

    That is, does it matter which region of storage the delete-expression deallocates, or is it expected to be the region of storage corresponding to the object to which the operand of the delete-expression points? Perhaps the current language should be replaced by its inversion:

    • a delete-expression ([expr.delete]), unless it which deallocates a region of storage not allocated within the evaluation of E;