Search code examples
c++windowsdynamic-memory-allocationnew-operatordefault-constructor

Is there a way to implement the same behaviour of the new operator calling default constructors?


I want to implement my own heap allocation function to do the same as the C++ new operator when it comes to allocating elements of a class type. My allocation function (for Windows) does not use the new operator or the standard library, but only HeapAlloc. So my problem is when allocating elements of a class type :

// Allocation function definition.
template <typename Type>
Type* Alloc(int Count) {
  return reinterpret_cast<Type*>(HeapAlloc(GetProcessHeap(), 0, sizeof(Type) * Count));
}

// Allocation function use.
Alloc<Class>(5);

The function allocates 5 Class, which is a simple class with a single default constructor. The constructor does not get called, and I can see why. The only thing I do is allocate some memory and reinterpret_cast it.

Where I get confused though, is with the new operator :

new Class[5];

The default constructor gets called! The new operator is in the end (on Windows of course) also calling HeapAlloc. The function does some other stuff too, but none of it seems to really have an impact on the final result.

How can new call the constructors of each element it allocates, and can I do the same? Or is this just some built-in feature (even though it's part of the standard library)?


Solution

  • So called new expressions (new int[5]) call operator new (similar to malloc) and the objects' constructor. You can use placement new expressions or std::construct_at from C++20 to call the constructors manually. Your code could end up looking like this:

    // Raw memory allocation function
    void* AllocBytes(size_t size, size_t align) {
        void* p = HeapAlloc(GetProcessHeap(), 0, align);
        assert(reinterpret_cast<uintptr_t>(p) % align == 0 && 
               "Invalid alignment");
        return p;
    }
    
    // Raw memory deallocation function
    void DeallocBytes(void* p, size_t size, size_t align) {
        // Use Windows API to deallocate memory
    }
    
    // Allocation AND construction function 
    template <typename Type>
    Type* Alloc(size_t Count) {
        // Allocate memory for 'Count' elements
        Type* array = reinterpret_cast<Type*>(AllocBytes(Count * sizeof(Type), alignof(Type)));
        // Construct 'Count' elements in the memory buffer
        for (size_t i = 0; i < count; ++i) {
            std::construct_at(&array[i]); // From <memory> in C++20
    
            // _Or_ a placement new expression
            ::new (&array[i]) Type();
        }
        return array;
    }
    
    // Destruction AND deallocation function 
    template <typename Type>
    void Dealloc(Type* p, size_t Count) {
        // Destroy 'Count' elements in the memory buffer
        for (size_t i = 0; i < count; ++i) {
            std::destroy_at(&array[i]); // From <memory> in C++17
    
            // _Or_ explicit destructor calls
            array[i].~Type();
        }
        DeallocBytes(p, Count * sizeof(Type), alignof(Type));
    }
    

    Then you can use the functions like this:

    // Allocate 5 elements
    Class* p = Alloc<Class>(5);
    
    // Deallocate 5 elements
    Dealloc(p, 5);
    

    Note that using std::construct_at/std::destroy_at or placement new expressions/manual destructor calls doesn't really make a difference. std::construct_at and std::destroy_at are constexpr and more explicit or perhaps easier to understand, but you can do it either way.