0

I have been browsing implementations of a vector / generalized memory management. I have found a question on codereview and I don't understand how one of the suggestions works.

Here is a snip of the relevant part of the question (the code):

template <class T>
class  Vector {
public:

    typedef T* Iterator;

    Vector();
    Vector(unsigned int size);
    Vector(unsigned int size, const T & initial);
    Vector(const Vector<T>& v);
    ~Vector();

    unsigned int capacity() const;
    unsigned int size() const;
    bool empty() const;
    Iterator begin();
    Iterator end();
    T& front();
    T& back();
    void push_back(const T& value);
    void pop_back();

    void reserve(unsigned int capacity);
    void resize(unsigned int size);

    T & operator[](unsigned int index);
    Vector<T> & operator = (const Vector<T> &);
    void clear();
private:
    unsigned int _size;
    unsigned int _capacity;
    unsigned int Log;
    T* buffer;
};

In reference to this answer, why does he recommend using a char* buffer rather than a T* buffer, and more importantly, how does this work and what does it mean? I understand that the char pointer has no initialization, but I would think that you could only use T*... how does a generic type fit into a char pointer?

Relevant part of the answer:

You are going to have a hard time making this work if the buffer is of type T. Every time you expand the buffer all the elements in the buffer will be initialized with T constructor. For int this is not a problem. But if T has a non trivial constructor then you are going to pay a heavy price initializing elements that may never be used.

T* buffer;

Really the buffer should be something that does not have a constructor.

char* buffer;
AlgoRythm
  • 1,196
  • 10
  • 31
  • May I ask why you create a vector class ? – adrien bedel Apr 13 '20 at 19:49
  • @adrienbedel To learn about generic types / templates. This is my first exercise with them. – AlgoRythm Apr 13 '20 at 19:50
  • You need to fully understand placement `new` to understand the purpose of using `char*` here. – François Andrieux Apr 13 '20 at 19:56
  • Real life implementations (like gcc's [`libstdc++`](https://github.com/gcc-mirror/gcc/blob/master/libstdc++-v3/include/bits/stl_vector.h)) don't use `char*`, but `T*`. The tip is is somewhat right however - you cannot just use `new T[capacity]`, because it will create too many objects. You need a buffer of memory, without real objects there. Using `char*` will achieve that, but a cost of losing type safety (which is bad). Placement `new` exists for this exact purpose. – Yksisarvinen Apr 13 '20 at 19:58
  • Possible duplicate of [What uses are there for “placement new”?](https://stackoverflow.com/questions/222557/what-uses-are-there-for-placement-new). – François Andrieux Apr 13 '20 at 19:59
  • @FrançoisAndrieux While placement new is an alternative to a raw char pointer, does it truly make this question a duplicate? My question was rather specific. – AlgoRythm Apr 13 '20 at 20:01
  • @AlgoRythm Maybe not, that's why I posted it as a comment first. But you said in a comment in the answers section bellow *"I have a decent idea of why but my main question is that I don't understand how a buffer of type char can hold type T "* which leads me to believe that placement new is what you are missing. – François Andrieux Apr 13 '20 at 20:02
  • @AlgoRythm Placement new is not an alternative to allocating a character array. It is what it required to make that strategy work. It isn't an alternative to using a raw char pointer, it's a complement to it. – François Andrieux Apr 13 '20 at 20:03
  • Was char type chosen randomly? Could integer type work as well? – AlgoRythm Apr 13 '20 at 20:03
  • @AlgoRythm To some limited extent yes, you could get away with using other types. But `char`, `unsigned char` and `std:byte` have a privileged position that allows them to be aliased by pointers to different types which allows you to do much more with them when they are used to represent raw memory. – François Andrieux Apr 13 '20 at 20:04
  • @FrançoisAndrieux Okay, that information, in combination with the edit to the answer from R Sahu have helped me understand. And as I understand, placement new is a system which allows me to more effectively achieve this design (though, I have not had a chance to fully research placement new yet, which is a new concept for me) – AlgoRythm Apr 13 '20 at 20:07
  • @AlgoRythm The basics of placement new is that instead of allocating and constructing an object in that fresh memory, you instead give it a pointer where you want it to construct the object. It does not allocate anything, it just starts the lifetime of an object in some preowned memory. It's the closest thing to directly calling a constructor you can do. You also have to take care to call the destructor explicitly instead of using `delete` or `delete[]` because those would try to incorrectly free the memory. It can be tricky to use correctly, watch out. – François Andrieux Apr 13 '20 at 20:09

1 Answers1

2

The explanation is right there in the answer,

You are going to have a hard time making this work if the buffer is of type T. Every time you expand the buffer all the elements in the buffer will be initialized with T constructor. For int this is not a problem. But if T has a non trivial constructor then you are going to pay a heavy price initializing elements that may never be used.

When you can use

char* buffer_;

all the unused elements of buffer_ will contain uninitialized data but that's ok. You don't pay the price of initializing each object with a non-trivial constructor that you must pay when you use

T* buffer_;

@FrançoisAndrieux brings up anothe valid point. If T is not default constructible, you won't be able to use new T[capacity] to allocate memory.

Regarding your comment, an an array of char objects can be used to hold any object. You just have to allocate the appopriate number of char objects. Instead of capacity number of T objects, you'll have to allocate capacity*sizeof(T) number of char objects.

R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • 1
    It's not just about performance and unnecessary costs. Consider a type that is not default constructable. Using a `T[]` wouldn't be feasible. You also want to be able to destroy elements when you `pop_back` without reallocating. Consider a vector of `unique_ptr`, file handles or other ressources that need to be properly destroyed when you expect them to. – François Andrieux Apr 13 '20 at 19:57
  • 1
    I have a decent idea of *why* but my main question is that I don't understand how a buffer of type `char` can hold type `T` – AlgoRythm Apr 13 '20 at 19:58
  • @AlgoRythm [Placement new](https://en.cppreference.com/w/cpp/language/new#Placement_new) – Axalo Apr 13 '20 at 20:01
  • 1
    Whenever I researched "char* buffer c++" or any combination thereof, I only got results talking about strings. Your last point really helped clarify – AlgoRythm Apr 13 '20 at 20:14
  • @AlgoRythm, glad I was able to help. – R Sahu Apr 13 '20 at 20:16
  • @RSahu Can you provide any further resources as to of why this works? I am trying to learn and I still can't google the right terms without strings dominating the results. – AlgoRythm Apr 13 '20 at 20:17
  • 1
    @AlgoRythm, the links for "placement new" should help. I am not sure what more I can add. – R Sahu Apr 13 '20 at 20:26