Search code examples
c++c++17new-operator

c++17: which versions of global operators new/delete must be replaced at least to cover all cases?


when globally overloading operators new and delete, which versions need to be implemented to cover all cases?

it seems before c++17 it was enough to implement void* operator new(size_t bytes) and void operator delete(void* ptr) since the array versions and the non-throwing versions of the default implementation were implemented using these two functions.

But what about c++17? for example https://en.cppreference.com/w/cpp/memory/new/operator_new doesn't mention that (4) would be implemented by calling (3) (wherease (2) is mentioned to be implemented by calling (1)).

So, in c++17, what's the minimum of operators that need to have all calls to versions of new and delete (other than classes with local overrides) be covered by the replacements?


Solution

  • From cppreference https://en.cppreference.com/w/cpp/memory/new/operator_new#Global_replacements (And a similar note with operator delete):

    The standard library implementations of the nothrow versions (5-8) directly calls the corresponding throwing versions (1-4). The standard library implementation of the throwing array versions (2,4) directly calls the corresponding single-object version (1,3). Thus, replacing the throwing single object allocation functions is sufficient to handle all allocations.

    So you need to replace these functions:

    void* operator new(std::size_t count);
    void* operator new(std::size_t count, std::align_val_t al);
    void operator delete(void* ptr) noexcept;
    void operator delete(void* ptr, std::align_val_t al) noexcept;
    

    And the rest will be implemented in terms of those four.

    If you don't have an aligned malloc-equivalent to replace the aligned versions with, here's a simple implementation of the aligned new/delete in terms of the non-aligned versions:

    #include <cstddef>
    #include <new>
    #include <memory>
    
    void* operator new(std::size_t count, std::align_val_t al) {
        std::size_t align = static_cast<std::size_t>(al);
        if (align <= __STDCPP_DEFAULT_NEW_ALIGNMENT__) [[unlikely]] return operator new(count);
        std::size_t actually_allocating = align + count;
        if (actually_allocating < count || (actually_allocating += sizeof(void*)) < (align + count)) [[unlikely]] {
            // overflow
            throw std::bad_alloc();
        }
    
        void* unaligned = operator new(actually_allocating);
        void* aligned = unaligned;
        std::align(align, 0, aligned, actually_allocating);
        // Store a pointer to the start of the aligned memory, to be retrieved by delete
        ::new (static_cast<void*>(static_cast<char*>(aligned) - sizeof(void*))) void*(unaligned);
        return aligned;
    }
    
    void operator delete(void* ptr, std::align_val_t al) noexcept {
        if (static_cast<std::size_t>(al) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) [[likely]] {
            ptr = *static_cast<void**>(static_cast<void*>(static_cast<char*>(ptr) - sizeof(void*)));
        }
        operator delete(ptr);
    }