The element without default constructor in the custom template container

Viewed 222

I need to create a simple template container, which can store any object of any type and could be used everywhere. So, I did something like this:

template <typename Type>
class Container {
public:
    Container() : arraySize(10) { valueWrappers = new Type[arraySize];}

    Container(const Container& other) { /* --- */}
    ~Container() { /* --- */}
    Container& operator=(const Container& other) { /* --- */}

    /* some functions */

private:    
    int arraySize;
    Type* valueWrappers;
};

Now I have the problem - when I'm trying to create my container using as template a class without default constructor, the compilation error appears:

class MyClass {
public:
    MyClass(int value) :v(value) { }

private:
    int v;
};

int main() {
    Container<MyClass> cont;

    return 0;
}

C2512 'MyClass': no appropriate default constructor available

The problem is that I need to initialize the array of "Type" values with something, but I don't no what I need to use. I can't use NULL because, in this case, Container will work only with pointers. So, can somebody give an advice, how am I able to do it? Or, maybe, there is another way to solve this task?

3 Answers

Based on your requirements, I think you're going to have to use placement new. Since you haven't provided all the relevant code, I'm going to do what I can.

First, you're going to have to allocate raw memory instead of using new directly.

Container() : arraySize(10) { valueWrappers = reinterpret_cast<Type*>(::operator new(sizeof(Type) * arraySize)); }

Now when you put something in your Container, you'll have to construct it in place, using something like the following:

new (valueWrappers + index) Type(arguments to type);

In your destructor, you'll need to explicitly call the destructors on any object that you used placement new for.

valueWrappers[index]->~Type();

Lastly, release the memory using ::operator delete.

::operator delete(valueWrappers);

Please bear in mind that this is a very quick and dirty answer, and this code can be hard to debug and maintain. You're going to have to keep track of what indexes in valueWrapper have been initialized and which haven't during cleanup. If possible, I highly recommend using something akin to std::vector, which handles all this complexity for you.

One option is to not allocate the array in the default constructor, but initialise valueWrappers to null instead. Another option is to not have a default constructor in your template. Third option is to keep your class as-is and simply document that the template is default constructible only if the type argument is default constructible.

You can use std::optional to defer initialization, which is guaranteed to handle object lifetime correctly. Letting a default constructed container have 10 elements is also a questionable choice — a (count) constructor may be preferable.

template <typename Type>
class Container {
    using elem_t = std::optional<Type>;

    std::size_t count{};
    std::unique_ptr<elem_t[]> elems{};
public:
    Container() = default;
    Container(std::size_t cnt)
        : count{cnt}
        , elems{std::make_unique<elem_t[]>(cnt)}
    {
    }
    // for example
    template <typename... Args>
    void construct_at(std::size_t pos, Args&&... args)
    {
        assert(pos < count);
        assert(!elems[pos]);

        elems[pos].emplace(std::forward<Args>(args)...);
    }
    // ...
};

Note that I used std::unique_ptr to simplify memory management; a pointer will also be OK, though apparently more error-prone. Now you can traverse the container and construct the elements:

class MyClass {
public:
    MyClass(int value) :v(value) { }
private:
    int v;
};

int main()
{
    Container<MyClass> cont(10);
    for (std::size_t i = 0; i < 10; ++i) {
        cont.construct_at(i, /* argument */);
    }
}
Related