Search code examples
c++arraysdynamic-memory-allocationdefault-constructor

Dynamic array creation without default constructor


Disclaimer: I already know that raw arrays are not first class elements in C++ and that in many places, we are expected to replace them with vectors. But I still hope an other way...

Context:

I am trying to build a multi-dimensional container library using contiguous data instead of vectors of vectors. A simple analogy is a 2D C array versus an array of pointers. The multi-dimensional part is handled as proxy classes over a 1D array. It implies the use of proxy iterators but my current problem is the handling of the underlying 1D array.

Ideally, that 1D array could use either static or automatic memory (and should not be freed after use) or private dynamic memory that should be. So far so good, I currently use a boolean (owning) to remember whether the array should be deleted. I need to manually manage that memory, because I want sub-arrays to re-use the memory of their containers and cannot use the copy semantics of standard containers.

Problem:

If the underlying data type has a default contructor, everything is fine, I can safely build a dynamic array with new[], and later free it with delete[]. But I also wonder whether it could be possible to only require a copyable type, and initialize the array with a default value (what std::vector can do).

Question:

How can I build a dynamic array of a non default constructible objects and instead use copy initialization from a default value ?

Current research:

Provided a default ctor is available for class T, I can write:

    template <typename T>
    class Holder
    {
        size_t sz;         // size of the array
        bool owning;       // true if the array should be deleted
        T* data;           // pointer to the underlying array

    public:
        // builds an array of default initialized data
        Holder(size_t sz) : sz(sz), owning(true) {
            data = new T[sz];
        }

        // uses an existing array. Do not own it by default but can steal
        // it if own is true
        Holder(T* data, size_t sz, bool own=false) : sz(sz),
            owning(own), data(data) {};

        // builds an array of data initialized from a default value
        Holder(const T&value, size_t sz) : sz(sz), owning(true) {
            // what can be done here if no default ctor is available?
        }

        ~Holder() {
            if (owning) {
                delete[] data;
            }
        }

        // Copy ctor. Will borrow the original array
        Holder(const Holder<T>& other) : sz(other.sz), owning(false), data(other.data){}

        // Move ctor
        Holder(Holder&& other) : sz(other.sz), owning(other.owning) {
            data = other.data;
            other.data = nullptr;
            other.owning = false;
        }
        // assignment operators and accessors omitted for brievety
        ...
    };

I also know that I can safely fill an unitialized array with std::uninitialized_fill, and later destroy its objects with std::destroy but I am unsure of how to correctly use that and whether it is compatible with new[] and delete[]. Said differently can I just delete an array allocated with operator new[] and initialized with uninitialized_fill?


Solution

  • I am unsure of how to correctly use that and whether it is compatible with new[] and delete[].

    It isn't compatible with new T[], but it is compatible with new char[].

    template <typename T>
    class Holder
    {
        size_t sz;         // size of the array
        bool owning;       // true if the array should be deleted
        T* data;           // pointer to the underlying array
    
        using storage_t = std::aligned_storage_t<sizeof(T), alignof(T)>;
    
        static T* alloc(size_t sz) { // or just use an Allocator
            return reinterpret_cast<T*>(reinterpret_cast<char*>(new storage_t[sz])); 
        }
    
    public:
        // builds an array of default initialized data
        Holder(size_t sz) requires std::default_initializable<T> : sz(sz), owning(true), data(alloc(sz)) {
            std::uninitialized_default_construct_n(data, sz);
        }
    
        // uses an existing array. Do not own it by default but can steal
        // it if own is true
        // This is now suspect, we should take a deleter here.
        // Holder(T* data, size_t sz, bool own=false) : sz(sz),
        //     owning(own), data(data) {};
    
        // uses an existing array. Do not own it
        Holder(T* data, size_t sz) : sz(sz), owning(false), data(data) {};
    
        // uses an existing array. Do not own it
        template <size_t N>
        Holder(T (&data)[N]) : sz(N), owning(false), data(data) {};
    
        // builds an array of data initialized from a default value
        Holder(const T&value, size_t sz) requires std::copy_constructible<T> : sz(sz), owning(true), data(alloc(sz)) {
            std::uninitialized_fill_n(data, sz, value);
        }
    
        ~Holder() {
            if (owning) {
                std::destroy_n(data, sz);
                delete[] reinterpret_cast<storage_t*>(data);
            }
        }
    
        // Copy ctor. Will borrow the original array
        Holder(const Holder<T>& other) : sz(other.sz), owning(false), data(other.data){}
    
        // Move ctor
        Holder(Holder&& other) : data(std::exchange(other.data, nullptr)) sz(other.sz), owning(std::exchange(other.owning, false)) {
        }
        // assignment operators and accessors omitted for brievety
        ...
    };