c++dynamic-memory-allocationunique-ptr

How to allocate for later placement new "as if by new"


We need to separate allocation and initialization of some heap storage. Unfortunately, client code uses delete p; to delete the pointer. If we had control of the deletion, we could allocate with ::operator new (sizeof(T), std::align_val_t(alignof(T))) and then use placement new and the corresponding global ::operator delete. But (if I understand correctly) delete might call T::operator delete, potentially requiring an allocation that came from T::operator new, with a complicated preference order between the different possible arguments for T::operator new and T::operator delete if there are multiple overloads.

Is there something like std::allocate_as_if_by_new<T>() that will do exactly the same allocation that new T(...) would do, but without initializing, or at least guarantee that (after initialization) delete p; will correctly deallocate it?


Solution

  • Short answer: No Yes, as long as you're very careful that the appropriate deallocation function will be called.


    Long answer:

    What pointers are valid operands for single-object delete expressions (e.g. delete ptr;) is covered in the standard by expr.delete:

    7.6.2.9 Unary Expressions - Delete [expr.delete] [emphasis mine]

    (2) [...] In a single-object delete expression, the value of the operand of delete may be a null pointer value, a pointer to a non-array object created by a previous new-expression, or a pointer to a subobject representing a base class of such an object. If not, the behavior is undefined. [...]

    So as long as the pointer comes from any new expression (which could also potentially be a new-placement expression) it should be well-defined.

    You do have to watch out though that delete ptr; will actually call a deallocation function that can properly deallocate the storage - which requires a bit of work.

    There are three gotchas that you'll need to look out for:


    So you need to mimic the same allocation that a non-placement new-expression would make, so that delete ptr calls the matching operator delete.

    This is covered in 7.6.2.8 New [expr.new], and basically boils down to one of two calls:

    operator new(sizeof(T))
    operator new(sizeof(T), std::align_val_t(alignof(T)))
    

    So a function like allocate_as_if_by_new<T> would need to be implemented like this to properly allocate storage as a non-placement new-expression would do:

    template<class T, class... Args>
    concept has_class_new = requires() {
        { T::operator new(std::declval<Args>()...) } -> std::same_as<void*>;
    };
    
    template<class T, class... Args>
    concept has_global_new = requires() {
        { ::operator new(std::declval<Args>()...) } -> std::same_as<void*>;
    };
    
    template<class T>
    requires(std::is_object_v<T> && !std::is_array_v<T>)
    void* allocate_as_if_by_new() {
        // class-type T's may have overloaded operator new / operator delete
        if constexpr(std::is_class_v<T> || std::is_union_v<T>) {
            if constexpr(alignof(T) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) {
                // T has new-extended alignment
                // => std::align_val_t overloads need to be checked first
                if constexpr(has_class_new<T, std::size_t, std::align_val_t>)
                    return T::operator new(sizeof(T), std::align_val_t(alignof(T)));
                else if constexpr(has_class_new<T, std::size_t>)
                    return T::operator new(sizeof(T));
            } else {
                // T has an alignment less than or equal to __STDCPP_DEFAULT_NEW_ALIGNMENT__
                // => prefer overloads without std::align_val_t
                if constexpr(has_class_new<T, std::size_t>)
                    return T::operator new(sizeof(T));
                else if constexpr(has_class_new<T, std::size_t, std::align_val_t>)
                    return T::operator new(sizeof(T), std::align_val_t(alignof(T)));       
            }
        }
    
        // either not a class-type, or a class-type without overloaded operator new
    
        // note that we still need to check for both overloads of operator new in both cases.
        // a program might only define a replacement for one of those functions,
        // which would cause the other one to be not defined.
    
        if constexpr(alignof(T) > __STDCPP_DEFAULT_NEW_ALIGNMENT__) {
            if constexpr(has_global_new<T, std::size_t, std::align_val_t>)
                return ::operator new(sizeof(T), std::align_val_t(alignof(T)));
            else if constexpr(has_global_new<T, std::size_t>)
                return ::operator new(sizeof(T));
        } else {
            if constexpr(has_global_new<T, std::size_t>)
                return ::operator new(sizeof(T));
            else if constexpr(has_global_new<T, std::size_t, std::align_val_t>)
                return ::operator new(sizeof(T), std::align_val_t(alignof(T)));
        }
    
        // should never happen
        throw std::bad_alloc();
    }
    

    Usage:

    void* ptr = allocate_as_if_by_new<Foo>();
    // ...
    Foo* foo = new(ptr) Foo();
    // ...
    delete foo;
    

    There are a few alternatives you might consider:

    1. Use an allocator

    Allocators split allocation and construction into separate steps (and destruction & deallocation).

    So assuming you can change your code to use std::allocator (or any other allocator that satisfies the allocator completeness requirements) then it would be possible to allocate T's without initializing them:

    template<class T>
    void example() {
        using alloc_traits = std::allocator_traits<std::allocator<T>>;
        std::allocator<T> alloc;
    
        T* ptr = alloc_traits::allocate(alloc, 1);
        // ptr is uninitialized
        
        // ...
    
        // construct a T at ptr
        alloc_traits::construct(alloc, ptr /* , ...constructor args */);
    
        // ...
    
        // "delete ptr;" replacement:
        alloc_traits::destroy(alloc, ptr);
        alloc_traits::deallocate(alloc, ptr, 1);
    }
    

    or given that you tagged your question with :

    template<class T>
    struct std_allocator_deleter {
        constexpr void operator()(T* ptr) const {
            using alloc_traits = std::allocator_traits<std::allocator<T>>;
            std::allocator<T> alloc;
    
            alloc_traits::destroy(alloc, ptr);
            alloc_traits::deallocate(alloc, ptr, 1);
        }
    };
    
    template<class T>
    std::unique_ptr<T, std_allocator_deleter<T>> allocate_unique() {
        using alloc_traits = std::allocator_traits<std::allocator<T>>;
        std::allocator<T> alloc;
    
        T* ptr = alloc_traits::allocate(alloc, 1);
        return std::unique_ptr<T, std_allocator_deleter<T>>(ptr);
    }
    
    
    template<class T>
    void example() {
        // allocate uninitialized storage
        auto ptr = allocate_unique<T>();
        // ...
        // construct a T 
        new(ptr.get()) T();
        // ...
        // ptr destructor automatically destroys & deallocates T
    }
    

    Note that you must ensure that you always create a new T object in that storage, otherwise the std_allocator_deleter deleter would have undefined behavior.

    2. Don't use a delete-expression

    A simple fix would be to just not use a delete-expression.
    If you explicitly call the destructor followed by a call to operator delete your program would also be well behaved:

    T* ptr = (T*)::operator new(sizeof(T));
    // ...
    new(ptr) T();
    // ...
    
    
    
    // maybe ok, maybe undefined behavior (depening on alignment requirements of T and if T has overloads for operator new / delete)
    // delete ptr;
    
    // always ok:
    ptr->~T();
    ::operator delete(ptr);
    
    3. Create an object with a new-expression and replace it

    If your T types are cheaply default constructible (or you can modify them to be so) then you could just use a normal new-expression and call its destructor immediately:

    template<class T>
    T* allocate_uninitialized() {
        T* ptr = new T;
        ptr->~T();
        return ptr;
    }
    
    template<class T>
    void example() {
        T* ptr = allocate_unitialized<T>();
        // ...
        new (ptr) T();
        // ...
        delete ptr;
    }
    

    Note that you must always place a new T object at the pointer returned by allocate_uninitialized() before you pass it to a delete-expression, otherwise the behavior is undefined.