0

I am creating a templated Vector class, however, when comparing its use to something such as std::vector, I noticed that it does not allow structs\classes without a default (emtpty) constructor. The error I will get is

error C2512: 'SomeStruct' : no appropriate default constructor available
  : while compiling class template member function 'Vector<Type>::Vector(void)'
  : see reference to class template instantiation 'Vector<Type>' being compiled

However, if I were to go and use std::vector, this would be allowed. Here is my test case

struct SomeStruct
{
    SomeStruct(int a){}
};
template<typename Type>
class Vector
{
public:
    Vector();

protected:
    Type* m_Data;
    unsigned int m_Count;
    unsigned int m_Capacity;
};
template<typename Type>
Vector<Type>::Vector()
{
    m_Capacity = 0;
    m_Count = 0;
    m_Data = new Type[m_Capacity];
}

void main()
{
    Vector<SomeStruct> test1;
}

How can I allow my templated Vector allow types without a default (empty) constructor?

(I know I could just use std::vector, but I am doing this to learn more about the language, and to run into cases like this)

templatetypedef
  • 362,284
  • 104
  • 897
  • 1,065
mmurphy
  • 1,327
  • 4
  • 15
  • 30
  • Look at how the standard library does it: separate allocation from construction and construct the elements yourself, manually. Then you can use whatever constructor you like. – Kerrek SB Jan 13 '12 at 05:06
  • 1
    Having to deal with placement new is exactly why you don't reinvent the wheel -- somebody else has already done it better. – ildjarn Jan 13 '12 at 05:11

5 Answers5

3

The reason why this doesn't work for types without default constructors is because of this line:

    m_Data = new Type[m_Capacity]; 

The above line fundamentally does two things: allocate enough memory to hold m_Capacity instances of Type, then constructing each Type so that they're ready to use. Since you can't actually provide any constructor arguments through this new[] syntax, default constructors are required when you use this.

The way std::vector (and the other standard containers) deals with this is by separating the memory allocation process and the construction process. That is, std::vector amortizes the cost of memory allocation by requesting large chunks of memory with "nothing" in it. Then std::vector uses placement new to construct objects directly in that memory.

So something like this might be going on inside a std::vector:

// HUGE SIMPLICATION OF WHAT HAPPENS!!!
// EXPOSITION ONLY!!!
// NOT TO BE USED IN ANY PRODUCTION CODE WHATSOEVER!!!
// (I haven't even considered exception safety, etc.)

template<typename T>
class Vector
{
private:
    T* allocate_memory(std::size_t numItems)
    {
        // Allocates memory without doing any construction
        return static_cast<T*>(::operator new(sizeof(T)*numItems));
    }

    void deallocate_memory()
    {
        ::operator delete(buffer);
    }
    // ...

public:
    void push_back(const T& obj)
    {
        if(theresNotEnoughRoom()) {
            std::size_t newCapacity = calculateNewCapacity();
            T* temp = allocate_memory(newCapacity);
            copyItemsToNewBuffer(temp);
            deallocate_memory(buffer);
            buffer = temp;
            bufferEnd = temp+newCapacity;
        }
        new (bufferEnd) T(obj); // Construct a new instance of T at end of buffer.
        ++bufferEnd;
    }

    void pop_back()
    {
        if(size() > 0) {
            --bufferEnd;
            bufferEnd->~T();
        }
    }

    // ...

private:
    T* buffer;
    T* bufferEnd;
    // ...
};

So what's going on here is that our hypothetical Vector class allocates a relatively large slab of memory, then as items are pushed or inserted, the class does placement new in the memory. So this eliminates the default constructor requirement, since we don't actually construct any objects unless requested by the caller.

As you can already see, a std::vector class needs to do quite a bit of bookkeeping to make what it does efficient and safe. That's why we urge people to use the standard containers instead of rolling out your own, unless you really know what you're doing. Making an efficient, safe, and useful vector class is a huge undertaking.

For an idea of what's involved, take a look at a paper called "Exception Safety: Concepts and Techniques" by Bjarne Stroustrup which discusses a "simple vector" implementation (section 3.1). You'll see that it's not a trivial thing to implement.

Community
  • 1
  • 1
In silico
  • 51,091
  • 10
  • 150
  • 143
  • I have been studying this example (which has been a huge help) and I think I understand most of it, however, I do not understand "bufferEnd" though. Could you go into more detail about how it is being used in the construction of the new instance (I have looked into placement new, though in this instance I am still confused). I also do not understand why you would seem to increase and decrease it with ++ and --. – mmurphy Jan 14 '12 at 03:17
  • 1
    @mmurphy: Since we have decoupled the memory allocation and the object construction process, you need to somehow keep track of what parts of the memory have objects in them already. Since arrays are contiguous, the memory between `buffer` and `bufferEnd-1` contains initialized objects, while memory at and after `bufferEnd` contains no objects. Note that the code example is incomplete and used for exposition only. If you want a more detailed discussion on how vectors are implemented, see [this paper](http://www2.research.att.com/~bs/except.pdf). I recommend that you read the whole paper. – In silico Jan 14 '12 at 03:33
  • I am still putting all of this together (reading posts here, the paper you linked, and other sources). From what I gather, doesn't you "allocate_memory" actually initalize the T to whatever "sizeof(T)*numItems" while only creating one instance of T? It would seem that way since you are using that parameter as "optional-initializer-expression-list" as mentioned here http://en.wikipedia.org/wiki/Placement_new – mmurphy Jan 14 '12 at 21:40
  • 1
    @mmurphy: No. The syntax `::operator new()` does not call any constructors. All it does is allocate memory, so it's like `malloc()`. Note that `::operator new()` has no `new-type-id`, so it can't possibly know which constructor to call. And since it has no `new-type-id`, I can't just tell it "give me enough memory to hold 10 instances of `T`" because it doesn't know what `T` is. That's why there's a `sizeof(T)*numItems` expression there. – In silico Jan 14 '12 at 23:43
  • Ah ok, that is understandable. I have read the document you linked, along with a lot of other implementations, and just have one question left, then I am going to accept the answer. If the memory is allocated via ::operator new, then I should only need to call delete in the descructor and during reallocation times, correct? The reason I ask is because I looked into east, and it is implemented as done in that paper. However, I never see a delete call anywhere – mmurphy Jan 16 '12 at 06:35
  • 1
    @mmurphy: If you're talking about the paper by Bjarne Stroustrup, then the `::operator delete` call would be called in the `alloc.deallocate()` function in `vector_base::~vector_base()`. It's taking advantage of the [RAII idiom](http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization) to ensure that we don't forget to `delete` the memory when needed. – In silico Jan 16 '12 at 22:20
  • Oh ok, I see. Thank you for all of the help, this helped me out a lot and I hope will help out more people in the future. – mmurphy Jan 17 '12 at 04:15
1

new Type[m_Capacity] creates an array of m_Capacity objects of type Type. That's not what you want. You want an empty vector, with enough raw memory for m_Capacity objects. You don't want the objects, you only want the memory.

There are several tools to obtain raw memory in C++: allocators, ::operator new or malloc. I suggest using ::operator new for now.

void* storage = ::operator new(sizeof(Type) * m_Capacity);

// and deallocation
::operator delete(storage);

Then, once you have raw memory available, you will need a way to construct objects in it to implement the rest of the vector functionality. This is done using placement-new, which is a form of new that simply calls a constructor at a certain address:

Type* obj = ::new(address) Type(arguments);

Destruction of objects is then done by an explicit call to the destructor, because you don't want to release the memory each time an element is destroyed.

obj->~T();
R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
1

std::vector doesn't use the default constructor because every time it needs to construct something, it uses the copy constructor (or whichever constructor you specify, thanks Kerrek SB, discussion below). So, you could make your vector class work by not using the default constructor in lines such as:

m_Data = new Type[m_Capacity];

You can use placement new, which lets you construct an object in already allocated memory. This allows you to call whichever constructor you desire, such as the copy constructor. This is done like so:

int typeSize = sizeof(Type);
char* buffer = new char[typeSize * 2];
Type* typeA = new(buffer) Type(default_value);
Type* typeB = new(&buffer[typeSize]) Type(default_value);

Two things are notable here: we call new once, allocating a piece of memory of size equal to 2 'Types'. We then use placement new to construct two instances in place, without calling the default constructor: rather, we call the copy constructor. In this way, we can construct many instances in an array without calling the default constructor.

Finally, you will need to delete the original allocation, not those made using placement new. Because deallocating the original allocation will not call the destructors for the instances you created in the block of memory, you will need to explicitly call their destructors.

Liam M
  • 5,306
  • 4
  • 39
  • 55
  • 1
    Modern `vector`s can use any constructor you like. – Kerrek SB Jan 13 '12 at 05:24
  • @KerrekSB Ah, my mistake. I've added an amendment to my post, somewhat. If you could supply me with a link explaining how this s possible, I'd appreciate it. – Liam M Jan 13 '12 at 05:32
  • Use `emplace` or `emplace_back`: `v.emplace_back(arg1, arg2, arg3);` calls the matching three-argument constructor directly. – Kerrek SB Jan 13 '12 at 05:55
0

put this into your code:

SomeStruct() = default;

which creates default constructor.

or this:

SomeStruct() {}

same thing.

codekiddy
  • 5,897
  • 9
  • 50
  • 80
0

If you remove m_Data = new Type[m_Capacity]; from your constructor, and delay this creation until later, it will work. However, as has been pointed out, std::vector has the same rule: if you had std::vector<SomeStruct> test1(10); you would get the same error.

Also, void main() is BAD. It should always be at least int main.

Yuushi
  • 25,132
  • 7
  • 63
  • 81