Modern Vector implementation

Clash Royale CLAN TAG#URR8PPP
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;
up vote
12
down vote
favorite
Please give me some feedback on my attempt at a vector. I wanted this to be the lowest level concept of a vector that I'm using, so I'm primarily concerned with speed, and I would build wrappers afterward for safety when its needed. This is why I'm not using the copy and swap idiom for the copy ctor. Anyway please critique and give explanations for changes where it can be improved for my goal.
vector.h:
#ifndef VECTOR_H
#define VECTOR_H
template<typename T>
class Vector
public:
typedef T* iterator;
typedef const T* const_iterator;
Vector();
Vector(unsigned int size);
Vector(unsigned int size, const T& initial);
Vector(const Vector<T>& v);
Vector(Vector<T> && v);
~Vector();
Vector<T>& operator =(Vector<T> const& v);
Vector<T>& operator =(Vector<T> && v); // move assignment
inline unsigned int capacity() const return m_capacity;
inline unsigned int size() const return m_size;
inline bool empty() const return m_size == 0;
inline iterator begin() return buff;
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_begin() return buff;
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
T& front();
T& back();
void pushBack(const T& val);
void popBack();
void reserve(unsigned int capacity);
void resize(unsigned int size);
T& operator (unsigned int index);
void clear();
private:
unsigned int m_capacity;
unsigned int m_size;
unsigned int Log;
T* buff;
void swap(Vector<T> & v);
;
#endif
vector.cpp:
#include "vector.h"
#include <cmath> //for Log
template<typename T>
Vector<T>::Vector() :
m_capacity(0),
m_size(0),
Log(0),
buff(nullptr)
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
template<typename T>
Vector<T>::Vector(unsigned int size, const T& initial) :
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
for (unsigned int i = 0; i < size; i++)
//using placement new to place each element of v[i] in our already allocated buffer
new(buffer + i) T(initial);
template<typename T>
Vector<T>::Vector(Vector<T> const& v) :
m_capacity(v.capacity()),
m_size(v.size()),
Log(v.Log),
buff(m_size ? new T[m_capacity] : nullptr)
std::copy(v.buff, v.buff + v.m_size, buff); //std::copy is better than loops if we have both arrays
//for (unsigned int i = 0; i < m_size; i++)
//new(buffer + sizeof(T)*i) T(v[i]);
template<typename T>
Vector<T>::Vector(Vector<T> && v)
: m_size(0),
m_capacity(0),
buff(nullptr),
Log(0)
swap(v);
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> const& v)
// //not strong exception, but will write a wrapper that makes it strong exception; lowest level -> fastest
delete buff;
buff = nullptr;
buff = v.m_capacity ? new T[v.m_capacity * sizeof(T)] : nullptr;
m_size = v.m_size;
m_capacity = v.m_capacity;
Log = v.Log;
std::copy(v.buff, v.buff + m_size-1, buff);
return *this;
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> && v)
delete buff; //prep this
m_size = 0;
buff = nullptr;
m_capacity = 0;
Log = 0;
swap(v);
return *this;
template<typename T>
Vector<T>::~Vector()
delete buff;
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
template<typename T>
T& Vector<T>::operator(unsigned int index)
return buff[index];
template<typename T>
T& Vector<T>::front()
return buff[0];
template<typename T>
T& Vector<T>::back()
return buff[m_size - 1];
template<typename T>
void Vector<T>::reserve(unsigned int capac)
T* newbuff = new T[capac];
for (unsigned int i = 0; i < m_size; i++)
newbuff[i] = buff[i];
m_capacity = capac;
delete buff;
buff = newbuff;
template<typename T>
void Vector<T>::resize(unsigned int size)
Log = ceil(log((double)size) / log(2.0));
reserve(1 << Log);
m_size = size;
template<typename T>
void Vector<T>::pushBack(const T& val)
if (m_size >= m_capacity)
reserve(1 << Log++);
buff[m_size++] = val;
template<typename T>
void Vector<T>::popBack()
(reinterpret_cast<T*>(buff)[m_size-- - 1]).~T();
template<typename T>
void Vector<T>::clear()
for (int i = 0; i < m_size; i++)
(reinterpret_cast<T*>(buff)[i]).~T();
m_size = 0;
m_capacity = 0;
Log = 0;
buff = 0;
template<typename T>
void Vector<T>::swap(Vector<T> & v)
std::swap(m_size, v.m_size);
std::swap(m_capacity, v.m_capacity);
std::swap(buff, v.buff);
std::swap(Log, v.Log);
c++ c++11 reinventing-the-wheel vectors
 |Â
show 2 more comments
up vote
12
down vote
favorite
Please give me some feedback on my attempt at a vector. I wanted this to be the lowest level concept of a vector that I'm using, so I'm primarily concerned with speed, and I would build wrappers afterward for safety when its needed. This is why I'm not using the copy and swap idiom for the copy ctor. Anyway please critique and give explanations for changes where it can be improved for my goal.
vector.h:
#ifndef VECTOR_H
#define VECTOR_H
template<typename T>
class Vector
public:
typedef T* iterator;
typedef const T* const_iterator;
Vector();
Vector(unsigned int size);
Vector(unsigned int size, const T& initial);
Vector(const Vector<T>& v);
Vector(Vector<T> && v);
~Vector();
Vector<T>& operator =(Vector<T> const& v);
Vector<T>& operator =(Vector<T> && v); // move assignment
inline unsigned int capacity() const return m_capacity;
inline unsigned int size() const return m_size;
inline bool empty() const return m_size == 0;
inline iterator begin() return buff;
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_begin() return buff;
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
T& front();
T& back();
void pushBack(const T& val);
void popBack();
void reserve(unsigned int capacity);
void resize(unsigned int size);
T& operator (unsigned int index);
void clear();
private:
unsigned int m_capacity;
unsigned int m_size;
unsigned int Log;
T* buff;
void swap(Vector<T> & v);
;
#endif
vector.cpp:
#include "vector.h"
#include <cmath> //for Log
template<typename T>
Vector<T>::Vector() :
m_capacity(0),
m_size(0),
Log(0),
buff(nullptr)
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
template<typename T>
Vector<T>::Vector(unsigned int size, const T& initial) :
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
for (unsigned int i = 0; i < size; i++)
//using placement new to place each element of v[i] in our already allocated buffer
new(buffer + i) T(initial);
template<typename T>
Vector<T>::Vector(Vector<T> const& v) :
m_capacity(v.capacity()),
m_size(v.size()),
Log(v.Log),
buff(m_size ? new T[m_capacity] : nullptr)
std::copy(v.buff, v.buff + v.m_size, buff); //std::copy is better than loops if we have both arrays
//for (unsigned int i = 0; i < m_size; i++)
//new(buffer + sizeof(T)*i) T(v[i]);
template<typename T>
Vector<T>::Vector(Vector<T> && v)
: m_size(0),
m_capacity(0),
buff(nullptr),
Log(0)
swap(v);
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> const& v)
// //not strong exception, but will write a wrapper that makes it strong exception; lowest level -> fastest
delete buff;
buff = nullptr;
buff = v.m_capacity ? new T[v.m_capacity * sizeof(T)] : nullptr;
m_size = v.m_size;
m_capacity = v.m_capacity;
Log = v.Log;
std::copy(v.buff, v.buff + m_size-1, buff);
return *this;
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> && v)
delete buff; //prep this
m_size = 0;
buff = nullptr;
m_capacity = 0;
Log = 0;
swap(v);
return *this;
template<typename T>
Vector<T>::~Vector()
delete buff;
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
template<typename T>
T& Vector<T>::operator(unsigned int index)
return buff[index];
template<typename T>
T& Vector<T>::front()
return buff[0];
template<typename T>
T& Vector<T>::back()
return buff[m_size - 1];
template<typename T>
void Vector<T>::reserve(unsigned int capac)
T* newbuff = new T[capac];
for (unsigned int i = 0; i < m_size; i++)
newbuff[i] = buff[i];
m_capacity = capac;
delete buff;
buff = newbuff;
template<typename T>
void Vector<T>::resize(unsigned int size)
Log = ceil(log((double)size) / log(2.0));
reserve(1 << Log);
m_size = size;
template<typename T>
void Vector<T>::pushBack(const T& val)
if (m_size >= m_capacity)
reserve(1 << Log++);
buff[m_size++] = val;
template<typename T>
void Vector<T>::popBack()
(reinterpret_cast<T*>(buff)[m_size-- - 1]).~T();
template<typename T>
void Vector<T>::clear()
for (int i = 0; i < m_size; i++)
(reinterpret_cast<T*>(buff)[i]).~T();
m_size = 0;
m_capacity = 0;
Log = 0;
buff = 0;
template<typename T>
void Vector<T>::swap(Vector<T> & v)
std::swap(m_size, v.m_size);
std::swap(m_capacity, v.m_capacity);
std::swap(buff, v.buff);
std::swap(Log, v.Log);
c++ c++11 reinventing-the-wheel vectors
4
Is there a reason for not simply usingstd::vector?
â Edward
Jul 12 at 12:19
6
If this is meant to be reinventing-the-wheel, you should add that tag (and consider using matching names such aspush_backandcbegin).
â Toby Speight
Jul 12 at 13:58
2
inlineas a keyword for functions defined in class is useless because these functions getinlineautomatic.
â Sandro4912
Jul 12 at 16:15
1
I don't think the code as posted was actually ready for a review. Please test your code before submitting it here.
â hoffmale
Jul 12 at 17:35
1
Why do you think that copy-and-swap idiom might reduce the speed?
â Toby Speight
Jul 12 at 18:15
 |Â
show 2 more comments
up vote
12
down vote
favorite
up vote
12
down vote
favorite
Please give me some feedback on my attempt at a vector. I wanted this to be the lowest level concept of a vector that I'm using, so I'm primarily concerned with speed, and I would build wrappers afterward for safety when its needed. This is why I'm not using the copy and swap idiom for the copy ctor. Anyway please critique and give explanations for changes where it can be improved for my goal.
vector.h:
#ifndef VECTOR_H
#define VECTOR_H
template<typename T>
class Vector
public:
typedef T* iterator;
typedef const T* const_iterator;
Vector();
Vector(unsigned int size);
Vector(unsigned int size, const T& initial);
Vector(const Vector<T>& v);
Vector(Vector<T> && v);
~Vector();
Vector<T>& operator =(Vector<T> const& v);
Vector<T>& operator =(Vector<T> && v); // move assignment
inline unsigned int capacity() const return m_capacity;
inline unsigned int size() const return m_size;
inline bool empty() const return m_size == 0;
inline iterator begin() return buff;
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_begin() return buff;
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
T& front();
T& back();
void pushBack(const T& val);
void popBack();
void reserve(unsigned int capacity);
void resize(unsigned int size);
T& operator (unsigned int index);
void clear();
private:
unsigned int m_capacity;
unsigned int m_size;
unsigned int Log;
T* buff;
void swap(Vector<T> & v);
;
#endif
vector.cpp:
#include "vector.h"
#include <cmath> //for Log
template<typename T>
Vector<T>::Vector() :
m_capacity(0),
m_size(0),
Log(0),
buff(nullptr)
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
template<typename T>
Vector<T>::Vector(unsigned int size, const T& initial) :
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
for (unsigned int i = 0; i < size; i++)
//using placement new to place each element of v[i] in our already allocated buffer
new(buffer + i) T(initial);
template<typename T>
Vector<T>::Vector(Vector<T> const& v) :
m_capacity(v.capacity()),
m_size(v.size()),
Log(v.Log),
buff(m_size ? new T[m_capacity] : nullptr)
std::copy(v.buff, v.buff + v.m_size, buff); //std::copy is better than loops if we have both arrays
//for (unsigned int i = 0; i < m_size; i++)
//new(buffer + sizeof(T)*i) T(v[i]);
template<typename T>
Vector<T>::Vector(Vector<T> && v)
: m_size(0),
m_capacity(0),
buff(nullptr),
Log(0)
swap(v);
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> const& v)
// //not strong exception, but will write a wrapper that makes it strong exception; lowest level -> fastest
delete buff;
buff = nullptr;
buff = v.m_capacity ? new T[v.m_capacity * sizeof(T)] : nullptr;
m_size = v.m_size;
m_capacity = v.m_capacity;
Log = v.Log;
std::copy(v.buff, v.buff + m_size-1, buff);
return *this;
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> && v)
delete buff; //prep this
m_size = 0;
buff = nullptr;
m_capacity = 0;
Log = 0;
swap(v);
return *this;
template<typename T>
Vector<T>::~Vector()
delete buff;
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
template<typename T>
T& Vector<T>::operator(unsigned int index)
return buff[index];
template<typename T>
T& Vector<T>::front()
return buff[0];
template<typename T>
T& Vector<T>::back()
return buff[m_size - 1];
template<typename T>
void Vector<T>::reserve(unsigned int capac)
T* newbuff = new T[capac];
for (unsigned int i = 0; i < m_size; i++)
newbuff[i] = buff[i];
m_capacity = capac;
delete buff;
buff = newbuff;
template<typename T>
void Vector<T>::resize(unsigned int size)
Log = ceil(log((double)size) / log(2.0));
reserve(1 << Log);
m_size = size;
template<typename T>
void Vector<T>::pushBack(const T& val)
if (m_size >= m_capacity)
reserve(1 << Log++);
buff[m_size++] = val;
template<typename T>
void Vector<T>::popBack()
(reinterpret_cast<T*>(buff)[m_size-- - 1]).~T();
template<typename T>
void Vector<T>::clear()
for (int i = 0; i < m_size; i++)
(reinterpret_cast<T*>(buff)[i]).~T();
m_size = 0;
m_capacity = 0;
Log = 0;
buff = 0;
template<typename T>
void Vector<T>::swap(Vector<T> & v)
std::swap(m_size, v.m_size);
std::swap(m_capacity, v.m_capacity);
std::swap(buff, v.buff);
std::swap(Log, v.Log);
c++ c++11 reinventing-the-wheel vectors
Please give me some feedback on my attempt at a vector. I wanted this to be the lowest level concept of a vector that I'm using, so I'm primarily concerned with speed, and I would build wrappers afterward for safety when its needed. This is why I'm not using the copy and swap idiom for the copy ctor. Anyway please critique and give explanations for changes where it can be improved for my goal.
vector.h:
#ifndef VECTOR_H
#define VECTOR_H
template<typename T>
class Vector
public:
typedef T* iterator;
typedef const T* const_iterator;
Vector();
Vector(unsigned int size);
Vector(unsigned int size, const T& initial);
Vector(const Vector<T>& v);
Vector(Vector<T> && v);
~Vector();
Vector<T>& operator =(Vector<T> const& v);
Vector<T>& operator =(Vector<T> && v); // move assignment
inline unsigned int capacity() const return m_capacity;
inline unsigned int size() const return m_size;
inline bool empty() const return m_size == 0;
inline iterator begin() return buff;
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_begin() return buff;
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
T& front();
T& back();
void pushBack(const T& val);
void popBack();
void reserve(unsigned int capacity);
void resize(unsigned int size);
T& operator (unsigned int index);
void clear();
private:
unsigned int m_capacity;
unsigned int m_size;
unsigned int Log;
T* buff;
void swap(Vector<T> & v);
;
#endif
vector.cpp:
#include "vector.h"
#include <cmath> //for Log
template<typename T>
Vector<T>::Vector() :
m_capacity(0),
m_size(0),
Log(0),
buff(nullptr)
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
template<typename T>
Vector<T>::Vector(unsigned int size, const T& initial) :
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
for (unsigned int i = 0; i < size; i++)
//using placement new to place each element of v[i] in our already allocated buffer
new(buffer + i) T(initial);
template<typename T>
Vector<T>::Vector(Vector<T> const& v) :
m_capacity(v.capacity()),
m_size(v.size()),
Log(v.Log),
buff(m_size ? new T[m_capacity] : nullptr)
std::copy(v.buff, v.buff + v.m_size, buff); //std::copy is better than loops if we have both arrays
//for (unsigned int i = 0; i < m_size; i++)
//new(buffer + sizeof(T)*i) T(v[i]);
template<typename T>
Vector<T>::Vector(Vector<T> && v)
: m_size(0),
m_capacity(0),
buff(nullptr),
Log(0)
swap(v);
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> const& v)
// //not strong exception, but will write a wrapper that makes it strong exception; lowest level -> fastest
delete buff;
buff = nullptr;
buff = v.m_capacity ? new T[v.m_capacity * sizeof(T)] : nullptr;
m_size = v.m_size;
m_capacity = v.m_capacity;
Log = v.Log;
std::copy(v.buff, v.buff + m_size-1, buff);
return *this;
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> && v)
delete buff; //prep this
m_size = 0;
buff = nullptr;
m_capacity = 0;
Log = 0;
swap(v);
return *this;
template<typename T>
Vector<T>::~Vector()
delete buff;
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
template<typename T>
T& Vector<T>::operator(unsigned int index)
return buff[index];
template<typename T>
T& Vector<T>::front()
return buff[0];
template<typename T>
T& Vector<T>::back()
return buff[m_size - 1];
template<typename T>
void Vector<T>::reserve(unsigned int capac)
T* newbuff = new T[capac];
for (unsigned int i = 0; i < m_size; i++)
newbuff[i] = buff[i];
m_capacity = capac;
delete buff;
buff = newbuff;
template<typename T>
void Vector<T>::resize(unsigned int size)
Log = ceil(log((double)size) / log(2.0));
reserve(1 << Log);
m_size = size;
template<typename T>
void Vector<T>::pushBack(const T& val)
if (m_size >= m_capacity)
reserve(1 << Log++);
buff[m_size++] = val;
template<typename T>
void Vector<T>::popBack()
(reinterpret_cast<T*>(buff)[m_size-- - 1]).~T();
template<typename T>
void Vector<T>::clear()
for (int i = 0; i < m_size; i++)
(reinterpret_cast<T*>(buff)[i]).~T();
m_size = 0;
m_capacity = 0;
Log = 0;
buff = 0;
template<typename T>
void Vector<T>::swap(Vector<T> & v)
std::swap(m_size, v.m_size);
std::swap(m_capacity, v.m_capacity);
std::swap(buff, v.buff);
std::swap(Log, v.Log);
c++ c++11 reinventing-the-wheel vectors
edited Jul 12 at 17:31
t3chb0t
31.8k54095
31.8k54095
asked Jul 12 at 11:39
Hayden Piper
6713
6713
4
Is there a reason for not simply usingstd::vector?
â Edward
Jul 12 at 12:19
6
If this is meant to be reinventing-the-wheel, you should add that tag (and consider using matching names such aspush_backandcbegin).
â Toby Speight
Jul 12 at 13:58
2
inlineas a keyword for functions defined in class is useless because these functions getinlineautomatic.
â Sandro4912
Jul 12 at 16:15
1
I don't think the code as posted was actually ready for a review. Please test your code before submitting it here.
â hoffmale
Jul 12 at 17:35
1
Why do you think that copy-and-swap idiom might reduce the speed?
â Toby Speight
Jul 12 at 18:15
 |Â
show 2 more comments
4
Is there a reason for not simply usingstd::vector?
â Edward
Jul 12 at 12:19
6
If this is meant to be reinventing-the-wheel, you should add that tag (and consider using matching names such aspush_backandcbegin).
â Toby Speight
Jul 12 at 13:58
2
inlineas a keyword for functions defined in class is useless because these functions getinlineautomatic.
â Sandro4912
Jul 12 at 16:15
1
I don't think the code as posted was actually ready for a review. Please test your code before submitting it here.
â hoffmale
Jul 12 at 17:35
1
Why do you think that copy-and-swap idiom might reduce the speed?
â Toby Speight
Jul 12 at 18:15
4
4
Is there a reason for not simply using
std::vector?â Edward
Jul 12 at 12:19
Is there a reason for not simply using
std::vector?â Edward
Jul 12 at 12:19
6
6
If this is meant to be reinventing-the-wheel, you should add that tag (and consider using matching names such as
push_back and cbegin).â Toby Speight
Jul 12 at 13:58
If this is meant to be reinventing-the-wheel, you should add that tag (and consider using matching names such as
push_back and cbegin).â Toby Speight
Jul 12 at 13:58
2
2
inline as a keyword for functions defined in class is useless because these functions get inline automatic.â Sandro4912
Jul 12 at 16:15
inline as a keyword for functions defined in class is useless because these functions get inline automatic.â Sandro4912
Jul 12 at 16:15
1
1
I don't think the code as posted was actually ready for a review. Please test your code before submitting it here.
â hoffmale
Jul 12 at 17:35
I don't think the code as posted was actually ready for a review. Please test your code before submitting it here.
â hoffmale
Jul 12 at 17:35
1
1
Why do you think that copy-and-swap idiom might reduce the speed?
â Toby Speight
Jul 12 at 18:15
Why do you think that copy-and-swap idiom might reduce the speed?
â Toby Speight
Jul 12 at 18:15
 |Â
show 2 more comments
6 Answers
6
active
oldest
votes
up vote
20
down vote
Memory management
new T[m_capacity]default-constructsm_capacityobjects of typeTin there.This might hurt performance a lot if
Ts default constructor is not trivial, and even then it still isn't necessary.Even worse, in
pushBacka newTobject gets copy constructed in the place of the already existing one (ifm_size < m_capacity), without properly deleting it. This might cause an abundance of errors, especially ifTs destructor is not trivial (e.g. it would leak memory).Consider using
std::aligned_storagefor storing the objects instead.The implementation leaks memory if an exception is thrown during any of the member functions which call
new(e.g. due to no being able to allocate new memory). Usingstd::unique_ptrhelps preventing those leaks.clearleaks memory (no call todelete).Some member functions (see below) set
m_capacity,Logand/orm_sizeto unexpected values, which in turn can cause wrong allocations and/or out of bounds memory accesses in other places.reserveaccesses out of bounds memory ifcapac < m_sizedue tonewbuff[i]being out of bounds.For any given
m_capacity, the first call topushBackwherem_size >= m_capacitydoesn't allocate bigger space (Loggets increased after the call toreserveinreserve(1 << Log++)).This means
Logis out of sync fromm_capacity,buff[m_size++]is out of bounds andm_size > m_capacityafter the call.The constructors taking a
sizeparameter initializem_capacitybeforeLog(due to member definition order inside the class), but refer to the uninitialized value ofLogfor doing so. This setsm_capacityto a wrong value, and thus allocates a wrong amount of memory forbuff(ifsize > 0).
pushBackhas a potential use-after-free error: Ifvalrefers to an element inside thisVectorandVectorneeds to reallocate,valwill dangle when the new object gets copy constructed.The copy assignment operator deletes
buffwithout checking whetherthis == &v. If that condition is true, it deletedv.buff(which it then will try to index in order to populate the newly allocatedbuff).- Also, that newly allocated
buffis too large by a factor ofsizeof(T).
- Also, that newly allocated
You might want to look into using
std::unique_ptr<T>forbuff. While this doesn't fix out of bounds memory access, it will help with some of the other issues.
Usage with standard algorithms
The name changes of
push_back,pop_back,cbeginandcendmake this container unusable for standard algorithms.beginandendshould provide aconstoverload, so they can be called on aconst Vector<T>&.Similarly,
cbeginandcendshould be declaredconst.Also,
push_front,pop_front,emplace,emplace_back,emplace_front,remove,insertand all the reverse iterator variants are missing.
Naming
- The style of names for member variables is inconsistent:
m_size/m_capacity,buffandLog. Choose one and stay consistent!
Class design
There is no way to handle types that have no default or no copy constructor. Please provide an overload on
push_back(T&&)to accept non-copyable types, andemplace_front/emplace_backmethods to constructTobjects for types that have neither a copy nor a move constructor. (Theemplace-type methods can also provide performance benefits by reducing copy/move constructions.)A
const Vector<T>&has no way to access elements inside it. Please provideconstoverloads forfront,backandoperator.Vector<T>::swapshould likely bepublic.Vector<T>isn't copyable unlessTitself is. Thus, the copy constructor and copy assignment operator should bedeleted for those cases.Why force the buffer to a size that is a power of 2? If I specify an exact size in the constructor (or via
reserve/resize), I'd expect theVectorto have exactly that size (and not waste additional memory).No member functions give any exception specification. This prevents certain kinds of optimization. Especially the move constructor, move assignment operator and destructor should be
noexcept.
Other issues
Why
reinterpret_cast<T*>(buff)?buffis already of typeT*.inlineactually doesn't do anything in this implementation.clear()doesn't call destructors for objects betweenm_sizeandm_capacity. (Then again, it doesn'tdelete buff, which would fix that.)Prefer
over()for initialization. While there is no instance of the most vexing parse in this code, it always helps to be mindful.As @TobySpeigh mentions, please put all code into the header file, as all code for templates needs to be accessible where instantiated. (Currently, one would have to use
#include "vector.cpp", which is surprising.)Iterators returned by
end()andcend()are expected to point one element after the last one (i.e.buff + m_size). Currently, they point wildly out of bounds (buff + (m_size - 1) * sizeof(T)).Sometimes
std::copyis used for copying, sometimes a rawforloop. This is inconsistent.In some of those cases, they could be replaced by a range-based
std::move(ifTis nothrow movable).In some cases, the last element isn't copied due to bound miscalculations.
Performance
No member function gives any exception specification. This might impede performance.
Why use floating point math for
Log? (And why useLogat all?)Try to
std::moveobjects if possible. This can be asserted with template meta programming.You might consider using
reallocfor reallocations ifTis trivially copyable.And as always, if performance is important, measure it! Doing premature optimization (especially at the cost of readability or extensibility) is bad.
Also, be sure your code is working correctly before trying to optimize. It doesn't matter if it is really fast if it produces the wrong result!
Many programmers prefer power-of-two heap allocations so as to minimize heap fragmentation. It also gets you O(log n) reallocation when repeatedly inserting.
â Davislor
Jul 12 at 22:30
1
@Davislor: Every constant growth factor provides $mathcalO(log n)$ reallocations upon insertion. And minimizing head fragmentation might be a good general goal, but not wasting memory if the user explicitly specifies their size requirements should be weighed higher IMHO.
â hoffmale
Jul 13 at 0:15
@hoffmale Every constant growth factor does, yes. Allocating only as much memory as you need, though, might need a billion allocations if you add a billion elements one-by-one. The counterargument to wasting memory was: if you do a lot of resizing or deallocation of small objects, you might in practice end up with lots of heap blocks that are too small to use, but that take a long time to search.
â Davislor
Jul 13 at 0:54
@hoffmale Added a section to my answer about that.
â Davislor
Jul 13 at 1:00
1
@Davislor Power of two heap allocations lead to the walking vector problem if you have one growing vector; no amount of previously freed vector buffers can fit your new memory request. Any sub power of 2 can avoid this, where after some number K iterations the previously deallocated K buffers can be used for the K+2nd exponential growth.
â Yakk
Jul 13 at 19:11
 |Â
show 5 more comments
up vote
11
down vote
A few things you can improve upon:
Use more of <algorithm> and <memory>
Raw loops are verbose and unreadable. Use algorithms whenever you can -I've just noticed that you did it in some cases, but left the raw loops in others (cf reserve). You also seem not to know std::uninitialized_fill, which does exactly what you do manually in your third constructor, or std::destroy that's here to achieve what you do in clear (std::destroy_at is for individual elements, as in popBack).
Factorize constructors
Constructors can call other constructors. It avoids some copy-pasting. So, given:
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
you can have:
template <typename T>
Vector<T>::Vector(unsigned size, const T& value) : Vector(size)
std::copy(buffer, buffer+size, value);
Simplify your destructor
You don't need to reset all member variables in your destructor, since you can't use it after anyway.
add a comment |Â
up vote
10
down vote
Use the well-known names for methods
c_begin and c_end are so close to the standard-container cbegin and cend that users will forever curse you. Likewise for pushBack and popBack (instead of the usual push_back and pop_back).
Use a consistent naming scheme within the class
Members seem to have a mix of naming schemes: buff, m_size, Log. Pick one and stick with it.
Avoid unnecessary code
In the destructor, we have:
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
These statements are useless clutter, as the object is being destroyed, and the members will not be accessible after this. A good compiler may elide the code for you, but it can't address the bigger problem, that there's more lines for the human reader to understand.
Similarly, in the move-constructor, we perform most of the destructor actions before calling swap(). It's generally expected that a moved-from object should soon be going out of scope, and perfectly normal to just swap the contents on the assumption that this tidy-up will occur then.
The clear() method leaks memory
We set buff to a null pointer without deleting its resource - there's a missing delete buff there. It's really worth running the unit tests under Valgrind occasionally to catch stuff like that.
Don't delete contents on assignment...
...until you have checked for self-assignment. That's one mistake that copy-and-swap will save you from.
It will help performance if you also check whether the current capacity is already suitable for the new contents. If it's at least the required size (and not too much bigger - e.g. one or two reallocation steps at most), then you can reduce the work of new and delete by simply re-using the storage you allocated.
Use the correct names of functions from <cmath>
It appears that your compiler exercises its right to define names in the global namespace as well as in std. But it's not portable to rely on that, so call std::log and std::ceil by their full names.
Use a single file
The method definitions need to be visible to the users (as they are templates), so they should be in the header file with the declarations.
Actually, there is aclear()member function...
â hoffmale
Jul 12 at 17:36
Thanks @hoffmale - now I see theclear()member, I see a bug in it, so I've edited accordingly.
â Toby Speight
Jul 12 at 18:11
add a comment |Â
up vote
9
down vote
There is a bug in pushBack, you need to take into account the case where you call it on an existing member of the array.
For example in the code
Vector<int> v(1,0);
v.pushBack(v[0]);
pushBack will first resize the array, invalidating the reference to v[0]. It will then attempt to insert that invalid reference in the new array.
Wow, totally overlooked that one with all the other memory management issues!
â hoffmale
Jul 12 at 17:51
Actually, this behavior is undefined in this specific case.Vector<int> v(1, 0)initializesm_capacitywrong, thusv.pushBack(v[0])might not actually reallocate.
â hoffmale
Jul 12 at 18:11
add a comment |Â
up vote
8
down vote
Problems
Your main issue is that your usage of placement new (and manual destructor call) are incorrect and undefined behavior results because the lifespan of objects is inconsistent.
Code Review
Seems a bit likely to clash with other people
#ifndef VECTOR_H
#define VECTOR_H
You have to make these unique include your namespace into the guard. Talking about namespace; add a namespace around your code.
Use of inline is discouraged.
inline unsigned int capacity() const return m_capacity;
inline unsigned int size() const return m_size;
inline bool empty() const return m_size == 0;
inline iterator begin() return buff;
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_begin() return buff;
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
In this context it does not add any real meaning. It was supposed to be a hint to the compiler to inline the code. But it was long since shown that humans are very bad at making this choice and all compilers ignore the hint and only inline when they determine it is useful.
These look wrong.
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
There is no need to add sizeof(T) in those expressions. When you increment a pointer you are adding by the size of the object that it points at.
Seems like you need const versions of these two (in addition to the non const versions).
T& front();
T& back();
T& operator (unsigned int index);
You have move constructors. Why not move insertion.
void pushBack(const T& val);
Why not just use a default parameter in the next constructor to implement this version of the constructor?
template<typename T>
Vector<T>::Vector() :
m_capacity(0),
m_size(0),
Log(0),
buff(nullptr)
The order of the initialization list is not relevant.
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
The members are initialized in the order they are declared in class declaration. Changing the order in the initializer list will not change that order. If you turn on your compiler warnings it will tell you this.
As a result these members are not all initialized correctly as some depend on values that have not been initialized yet.
The actual order that will be done is:
m_capacity(1 << Log), // Notice here Log is used
// But has not been initialized.
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
buff(size ? new T[m_capacity] : nullptr)
Incorrect usage of placement new.
buff(size ? new T[m_capacity] : nullptr) // You are calling the constructor of `T` for every object in this array
....
new(buffer + i) T(initial); // You are over writing an object that has had its constructor called.
Placement new can only be used on RAW memory. You are using it here on an already live object (an object that has had its constructor already called). This is undefined behavior.
It would have been more correct to use the assignment operator.
It is a good idea to mark the move constructor as noexcept.
template<typename T>
Vector<T>::Vector(Vector<T> && v)
: m_size(0),
m_capacity(0),
buff(nullptr),
Log(0)
swap(v);
This allows for certain optimizations by the compiler. Since the only call is to swap() this should be true (traditionally swap is also noexcept).
The Assignment operator does not provide the strong exception guarantee. Also it is not self assignment safe. If you assign this object to itself it will have undefined behavior.
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> const& v)
// //not strong exception, but will write a wrapper that makes it strong exception; lowest level -> fastest
delete buff;
buff = nullptr;
buff = v.m_capacity ? new T[v.m_capacity * sizeof(T)] : nullptr;
m_size = v.m_size;
m_capacity = v.m_capacity;
Log = v.Log;
std::copy(v.buff, v.buff + m_size-1, buff);
return *this;
I see you notice that in your comments. It is trivial to make this exception safe by using the copy and swap idiom (and it has no more overhead than doing it manually). Also the copy and swap idiom is self assignment safe.
Again move operator. Usually mark as noexcept. Unfortunately as you call delete you can't do that!
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> && v)
delete buff; //prep this
m_size = 0;
buff = nullptr;
m_capacity = 0;
Log = 0;
swap(v);
return *this;
There is no need to do the delete here. Simply do the swap. This will allow you to mark the method noexcept. After you have done the swap call clear on the RHS to force the objects in the vector to be destroyed (to mimic the requirements on the std::vector). Let the destructor of the RHS clean up the memory for the vector.
It also adds the opportunity to optimize the usage (as the storage can potentially be re-used).
Extra work being done.
template<typename T>
Vector<T>::~Vector()
delete buff;
// Everything below this point is a waste of time.
// Don't do it.
// Also assigning null to the pointer can potentially hide errors. When you are debugging.
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
This call to the destructor will get you into trouble.
template<typename T>
void Vector<T>::popBack()
(reinterpret_cast<T*>(buff)[m_size-- - 1]).~T();
The trouble is that buff is a pointer to T so when you call delete in the destructor it is going call the destructor for T. By calling it here you have ended the objects life span so calling delete in the destructor is undefined behavior.
I wrote 5 articles about building a vector.
Vector - Resource Management Allocation
Vector - Resource Management Copy Swap
Vector - Resize
Vector - Simple Optimizations
Vector - The Other Stuff
add a comment |Â
up vote
7
down vote
The Type of Your Size Parameters
Currently, you have them as unsigned int, but this is often different from size_t and ptrdiff_t. The most common example in 2018 is that most 64-bit compilers define unsigned int as 32 bits wide, which limits your vectors to 4GiB. It's also possible to imagine implementations with 16- or 24-bit size_t but a wider ALU (a 16-bit Windows or DOS program running on an 80386 or better?) The STL version uses size_t, which is 32 bits wide on 32-bit platforms and 64 bits wide on 64-bit platforms.
You will also often find some people who prefer signed to unsigned indices, on the grounds that it's very easy for arithmetic on indices to give you a negative number, and interpreting that as a very large unsigned number will lead to logic errors. This has a portability issue of its own, in that unsigned overflow and underflow are well-defined but signed overflow and underflow are not, but nearly all real-world implementations use twoâÂÂs-complement math. If you want to use signed indices, the type for that is ptrdiff_t, which is the type of the result of subtracting one pointer from another.
MicrosoftâÂÂs preferred solution is rsize_t, an optional annex to the C language standard. This is an unsigned quantity whose upper bit flags it as invalid (that is, a size_t that the library is meant to rigorously check for overflow).
Allocation Size
You choose to allocate blocks as powers of two. Elsewhere in the comments, when someone asked if it wouldnâÂÂt be better to allocate the exact amount of memory requested, I explain the arguments in favor of the power-of-two approach.
However, adding a .shrink_to_fit() method gives the client code the ability to decide that wasted memory is a bigger concern for that application than heap fragmentation. If the application doesnâÂÂt call that, they get the default behavior.
add a comment |Â
6 Answers
6
active
oldest
votes
6 Answers
6
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
20
down vote
Memory management
new T[m_capacity]default-constructsm_capacityobjects of typeTin there.This might hurt performance a lot if
Ts default constructor is not trivial, and even then it still isn't necessary.Even worse, in
pushBacka newTobject gets copy constructed in the place of the already existing one (ifm_size < m_capacity), without properly deleting it. This might cause an abundance of errors, especially ifTs destructor is not trivial (e.g. it would leak memory).Consider using
std::aligned_storagefor storing the objects instead.The implementation leaks memory if an exception is thrown during any of the member functions which call
new(e.g. due to no being able to allocate new memory). Usingstd::unique_ptrhelps preventing those leaks.clearleaks memory (no call todelete).Some member functions (see below) set
m_capacity,Logand/orm_sizeto unexpected values, which in turn can cause wrong allocations and/or out of bounds memory accesses in other places.reserveaccesses out of bounds memory ifcapac < m_sizedue tonewbuff[i]being out of bounds.For any given
m_capacity, the first call topushBackwherem_size >= m_capacitydoesn't allocate bigger space (Loggets increased after the call toreserveinreserve(1 << Log++)).This means
Logis out of sync fromm_capacity,buff[m_size++]is out of bounds andm_size > m_capacityafter the call.The constructors taking a
sizeparameter initializem_capacitybeforeLog(due to member definition order inside the class), but refer to the uninitialized value ofLogfor doing so. This setsm_capacityto a wrong value, and thus allocates a wrong amount of memory forbuff(ifsize > 0).
pushBackhas a potential use-after-free error: Ifvalrefers to an element inside thisVectorandVectorneeds to reallocate,valwill dangle when the new object gets copy constructed.The copy assignment operator deletes
buffwithout checking whetherthis == &v. If that condition is true, it deletedv.buff(which it then will try to index in order to populate the newly allocatedbuff).- Also, that newly allocated
buffis too large by a factor ofsizeof(T).
- Also, that newly allocated
You might want to look into using
std::unique_ptr<T>forbuff. While this doesn't fix out of bounds memory access, it will help with some of the other issues.
Usage with standard algorithms
The name changes of
push_back,pop_back,cbeginandcendmake this container unusable for standard algorithms.beginandendshould provide aconstoverload, so they can be called on aconst Vector<T>&.Similarly,
cbeginandcendshould be declaredconst.Also,
push_front,pop_front,emplace,emplace_back,emplace_front,remove,insertand all the reverse iterator variants are missing.
Naming
- The style of names for member variables is inconsistent:
m_size/m_capacity,buffandLog. Choose one and stay consistent!
Class design
There is no way to handle types that have no default or no copy constructor. Please provide an overload on
push_back(T&&)to accept non-copyable types, andemplace_front/emplace_backmethods to constructTobjects for types that have neither a copy nor a move constructor. (Theemplace-type methods can also provide performance benefits by reducing copy/move constructions.)A
const Vector<T>&has no way to access elements inside it. Please provideconstoverloads forfront,backandoperator.Vector<T>::swapshould likely bepublic.Vector<T>isn't copyable unlessTitself is. Thus, the copy constructor and copy assignment operator should bedeleted for those cases.Why force the buffer to a size that is a power of 2? If I specify an exact size in the constructor (or via
reserve/resize), I'd expect theVectorto have exactly that size (and not waste additional memory).No member functions give any exception specification. This prevents certain kinds of optimization. Especially the move constructor, move assignment operator and destructor should be
noexcept.
Other issues
Why
reinterpret_cast<T*>(buff)?buffis already of typeT*.inlineactually doesn't do anything in this implementation.clear()doesn't call destructors for objects betweenm_sizeandm_capacity. (Then again, it doesn'tdelete buff, which would fix that.)Prefer
over()for initialization. While there is no instance of the most vexing parse in this code, it always helps to be mindful.As @TobySpeigh mentions, please put all code into the header file, as all code for templates needs to be accessible where instantiated. (Currently, one would have to use
#include "vector.cpp", which is surprising.)Iterators returned by
end()andcend()are expected to point one element after the last one (i.e.buff + m_size). Currently, they point wildly out of bounds (buff + (m_size - 1) * sizeof(T)).Sometimes
std::copyis used for copying, sometimes a rawforloop. This is inconsistent.In some of those cases, they could be replaced by a range-based
std::move(ifTis nothrow movable).In some cases, the last element isn't copied due to bound miscalculations.
Performance
No member function gives any exception specification. This might impede performance.
Why use floating point math for
Log? (And why useLogat all?)Try to
std::moveobjects if possible. This can be asserted with template meta programming.You might consider using
reallocfor reallocations ifTis trivially copyable.And as always, if performance is important, measure it! Doing premature optimization (especially at the cost of readability or extensibility) is bad.
Also, be sure your code is working correctly before trying to optimize. It doesn't matter if it is really fast if it produces the wrong result!
Many programmers prefer power-of-two heap allocations so as to minimize heap fragmentation. It also gets you O(log n) reallocation when repeatedly inserting.
â Davislor
Jul 12 at 22:30
1
@Davislor: Every constant growth factor provides $mathcalO(log n)$ reallocations upon insertion. And minimizing head fragmentation might be a good general goal, but not wasting memory if the user explicitly specifies their size requirements should be weighed higher IMHO.
â hoffmale
Jul 13 at 0:15
@hoffmale Every constant growth factor does, yes. Allocating only as much memory as you need, though, might need a billion allocations if you add a billion elements one-by-one. The counterargument to wasting memory was: if you do a lot of resizing or deallocation of small objects, you might in practice end up with lots of heap blocks that are too small to use, but that take a long time to search.
â Davislor
Jul 13 at 0:54
@hoffmale Added a section to my answer about that.
â Davislor
Jul 13 at 1:00
1
@Davislor Power of two heap allocations lead to the walking vector problem if you have one growing vector; no amount of previously freed vector buffers can fit your new memory request. Any sub power of 2 can avoid this, where after some number K iterations the previously deallocated K buffers can be used for the K+2nd exponential growth.
â Yakk
Jul 13 at 19:11
 |Â
show 5 more comments
up vote
20
down vote
Memory management
new T[m_capacity]default-constructsm_capacityobjects of typeTin there.This might hurt performance a lot if
Ts default constructor is not trivial, and even then it still isn't necessary.Even worse, in
pushBacka newTobject gets copy constructed in the place of the already existing one (ifm_size < m_capacity), without properly deleting it. This might cause an abundance of errors, especially ifTs destructor is not trivial (e.g. it would leak memory).Consider using
std::aligned_storagefor storing the objects instead.The implementation leaks memory if an exception is thrown during any of the member functions which call
new(e.g. due to no being able to allocate new memory). Usingstd::unique_ptrhelps preventing those leaks.clearleaks memory (no call todelete).Some member functions (see below) set
m_capacity,Logand/orm_sizeto unexpected values, which in turn can cause wrong allocations and/or out of bounds memory accesses in other places.reserveaccesses out of bounds memory ifcapac < m_sizedue tonewbuff[i]being out of bounds.For any given
m_capacity, the first call topushBackwherem_size >= m_capacitydoesn't allocate bigger space (Loggets increased after the call toreserveinreserve(1 << Log++)).This means
Logis out of sync fromm_capacity,buff[m_size++]is out of bounds andm_size > m_capacityafter the call.The constructors taking a
sizeparameter initializem_capacitybeforeLog(due to member definition order inside the class), but refer to the uninitialized value ofLogfor doing so. This setsm_capacityto a wrong value, and thus allocates a wrong amount of memory forbuff(ifsize > 0).
pushBackhas a potential use-after-free error: Ifvalrefers to an element inside thisVectorandVectorneeds to reallocate,valwill dangle when the new object gets copy constructed.The copy assignment operator deletes
buffwithout checking whetherthis == &v. If that condition is true, it deletedv.buff(which it then will try to index in order to populate the newly allocatedbuff).- Also, that newly allocated
buffis too large by a factor ofsizeof(T).
- Also, that newly allocated
You might want to look into using
std::unique_ptr<T>forbuff. While this doesn't fix out of bounds memory access, it will help with some of the other issues.
Usage with standard algorithms
The name changes of
push_back,pop_back,cbeginandcendmake this container unusable for standard algorithms.beginandendshould provide aconstoverload, so they can be called on aconst Vector<T>&.Similarly,
cbeginandcendshould be declaredconst.Also,
push_front,pop_front,emplace,emplace_back,emplace_front,remove,insertand all the reverse iterator variants are missing.
Naming
- The style of names for member variables is inconsistent:
m_size/m_capacity,buffandLog. Choose one and stay consistent!
Class design
There is no way to handle types that have no default or no copy constructor. Please provide an overload on
push_back(T&&)to accept non-copyable types, andemplace_front/emplace_backmethods to constructTobjects for types that have neither a copy nor a move constructor. (Theemplace-type methods can also provide performance benefits by reducing copy/move constructions.)A
const Vector<T>&has no way to access elements inside it. Please provideconstoverloads forfront,backandoperator.Vector<T>::swapshould likely bepublic.Vector<T>isn't copyable unlessTitself is. Thus, the copy constructor and copy assignment operator should bedeleted for those cases.Why force the buffer to a size that is a power of 2? If I specify an exact size in the constructor (or via
reserve/resize), I'd expect theVectorto have exactly that size (and not waste additional memory).No member functions give any exception specification. This prevents certain kinds of optimization. Especially the move constructor, move assignment operator and destructor should be
noexcept.
Other issues
Why
reinterpret_cast<T*>(buff)?buffis already of typeT*.inlineactually doesn't do anything in this implementation.clear()doesn't call destructors for objects betweenm_sizeandm_capacity. (Then again, it doesn'tdelete buff, which would fix that.)Prefer
over()for initialization. While there is no instance of the most vexing parse in this code, it always helps to be mindful.As @TobySpeigh mentions, please put all code into the header file, as all code for templates needs to be accessible where instantiated. (Currently, one would have to use
#include "vector.cpp", which is surprising.)Iterators returned by
end()andcend()are expected to point one element after the last one (i.e.buff + m_size). Currently, they point wildly out of bounds (buff + (m_size - 1) * sizeof(T)).Sometimes
std::copyis used for copying, sometimes a rawforloop. This is inconsistent.In some of those cases, they could be replaced by a range-based
std::move(ifTis nothrow movable).In some cases, the last element isn't copied due to bound miscalculations.
Performance
No member function gives any exception specification. This might impede performance.
Why use floating point math for
Log? (And why useLogat all?)Try to
std::moveobjects if possible. This can be asserted with template meta programming.You might consider using
reallocfor reallocations ifTis trivially copyable.And as always, if performance is important, measure it! Doing premature optimization (especially at the cost of readability or extensibility) is bad.
Also, be sure your code is working correctly before trying to optimize. It doesn't matter if it is really fast if it produces the wrong result!
Many programmers prefer power-of-two heap allocations so as to minimize heap fragmentation. It also gets you O(log n) reallocation when repeatedly inserting.
â Davislor
Jul 12 at 22:30
1
@Davislor: Every constant growth factor provides $mathcalO(log n)$ reallocations upon insertion. And minimizing head fragmentation might be a good general goal, but not wasting memory if the user explicitly specifies their size requirements should be weighed higher IMHO.
â hoffmale
Jul 13 at 0:15
@hoffmale Every constant growth factor does, yes. Allocating only as much memory as you need, though, might need a billion allocations if you add a billion elements one-by-one. The counterargument to wasting memory was: if you do a lot of resizing or deallocation of small objects, you might in practice end up with lots of heap blocks that are too small to use, but that take a long time to search.
â Davislor
Jul 13 at 0:54
@hoffmale Added a section to my answer about that.
â Davislor
Jul 13 at 1:00
1
@Davislor Power of two heap allocations lead to the walking vector problem if you have one growing vector; no amount of previously freed vector buffers can fit your new memory request. Any sub power of 2 can avoid this, where after some number K iterations the previously deallocated K buffers can be used for the K+2nd exponential growth.
â Yakk
Jul 13 at 19:11
 |Â
show 5 more comments
up vote
20
down vote
up vote
20
down vote
Memory management
new T[m_capacity]default-constructsm_capacityobjects of typeTin there.This might hurt performance a lot if
Ts default constructor is not trivial, and even then it still isn't necessary.Even worse, in
pushBacka newTobject gets copy constructed in the place of the already existing one (ifm_size < m_capacity), without properly deleting it. This might cause an abundance of errors, especially ifTs destructor is not trivial (e.g. it would leak memory).Consider using
std::aligned_storagefor storing the objects instead.The implementation leaks memory if an exception is thrown during any of the member functions which call
new(e.g. due to no being able to allocate new memory). Usingstd::unique_ptrhelps preventing those leaks.clearleaks memory (no call todelete).Some member functions (see below) set
m_capacity,Logand/orm_sizeto unexpected values, which in turn can cause wrong allocations and/or out of bounds memory accesses in other places.reserveaccesses out of bounds memory ifcapac < m_sizedue tonewbuff[i]being out of bounds.For any given
m_capacity, the first call topushBackwherem_size >= m_capacitydoesn't allocate bigger space (Loggets increased after the call toreserveinreserve(1 << Log++)).This means
Logis out of sync fromm_capacity,buff[m_size++]is out of bounds andm_size > m_capacityafter the call.The constructors taking a
sizeparameter initializem_capacitybeforeLog(due to member definition order inside the class), but refer to the uninitialized value ofLogfor doing so. This setsm_capacityto a wrong value, and thus allocates a wrong amount of memory forbuff(ifsize > 0).
pushBackhas a potential use-after-free error: Ifvalrefers to an element inside thisVectorandVectorneeds to reallocate,valwill dangle when the new object gets copy constructed.The copy assignment operator deletes
buffwithout checking whetherthis == &v. If that condition is true, it deletedv.buff(which it then will try to index in order to populate the newly allocatedbuff).- Also, that newly allocated
buffis too large by a factor ofsizeof(T).
- Also, that newly allocated
You might want to look into using
std::unique_ptr<T>forbuff. While this doesn't fix out of bounds memory access, it will help with some of the other issues.
Usage with standard algorithms
The name changes of
push_back,pop_back,cbeginandcendmake this container unusable for standard algorithms.beginandendshould provide aconstoverload, so they can be called on aconst Vector<T>&.Similarly,
cbeginandcendshould be declaredconst.Also,
push_front,pop_front,emplace,emplace_back,emplace_front,remove,insertand all the reverse iterator variants are missing.
Naming
- The style of names for member variables is inconsistent:
m_size/m_capacity,buffandLog. Choose one and stay consistent!
Class design
There is no way to handle types that have no default or no copy constructor. Please provide an overload on
push_back(T&&)to accept non-copyable types, andemplace_front/emplace_backmethods to constructTobjects for types that have neither a copy nor a move constructor. (Theemplace-type methods can also provide performance benefits by reducing copy/move constructions.)A
const Vector<T>&has no way to access elements inside it. Please provideconstoverloads forfront,backandoperator.Vector<T>::swapshould likely bepublic.Vector<T>isn't copyable unlessTitself is. Thus, the copy constructor and copy assignment operator should bedeleted for those cases.Why force the buffer to a size that is a power of 2? If I specify an exact size in the constructor (or via
reserve/resize), I'd expect theVectorto have exactly that size (and not waste additional memory).No member functions give any exception specification. This prevents certain kinds of optimization. Especially the move constructor, move assignment operator and destructor should be
noexcept.
Other issues
Why
reinterpret_cast<T*>(buff)?buffis already of typeT*.inlineactually doesn't do anything in this implementation.clear()doesn't call destructors for objects betweenm_sizeandm_capacity. (Then again, it doesn'tdelete buff, which would fix that.)Prefer
over()for initialization. While there is no instance of the most vexing parse in this code, it always helps to be mindful.As @TobySpeigh mentions, please put all code into the header file, as all code for templates needs to be accessible where instantiated. (Currently, one would have to use
#include "vector.cpp", which is surprising.)Iterators returned by
end()andcend()are expected to point one element after the last one (i.e.buff + m_size). Currently, they point wildly out of bounds (buff + (m_size - 1) * sizeof(T)).Sometimes
std::copyis used for copying, sometimes a rawforloop. This is inconsistent.In some of those cases, they could be replaced by a range-based
std::move(ifTis nothrow movable).In some cases, the last element isn't copied due to bound miscalculations.
Performance
No member function gives any exception specification. This might impede performance.
Why use floating point math for
Log? (And why useLogat all?)Try to
std::moveobjects if possible. This can be asserted with template meta programming.You might consider using
reallocfor reallocations ifTis trivially copyable.And as always, if performance is important, measure it! Doing premature optimization (especially at the cost of readability or extensibility) is bad.
Also, be sure your code is working correctly before trying to optimize. It doesn't matter if it is really fast if it produces the wrong result!
Memory management
new T[m_capacity]default-constructsm_capacityobjects of typeTin there.This might hurt performance a lot if
Ts default constructor is not trivial, and even then it still isn't necessary.Even worse, in
pushBacka newTobject gets copy constructed in the place of the already existing one (ifm_size < m_capacity), without properly deleting it. This might cause an abundance of errors, especially ifTs destructor is not trivial (e.g. it would leak memory).Consider using
std::aligned_storagefor storing the objects instead.The implementation leaks memory if an exception is thrown during any of the member functions which call
new(e.g. due to no being able to allocate new memory). Usingstd::unique_ptrhelps preventing those leaks.clearleaks memory (no call todelete).Some member functions (see below) set
m_capacity,Logand/orm_sizeto unexpected values, which in turn can cause wrong allocations and/or out of bounds memory accesses in other places.reserveaccesses out of bounds memory ifcapac < m_sizedue tonewbuff[i]being out of bounds.For any given
m_capacity, the first call topushBackwherem_size >= m_capacitydoesn't allocate bigger space (Loggets increased after the call toreserveinreserve(1 << Log++)).This means
Logis out of sync fromm_capacity,buff[m_size++]is out of bounds andm_size > m_capacityafter the call.The constructors taking a
sizeparameter initializem_capacitybeforeLog(due to member definition order inside the class), but refer to the uninitialized value ofLogfor doing so. This setsm_capacityto a wrong value, and thus allocates a wrong amount of memory forbuff(ifsize > 0).
pushBackhas a potential use-after-free error: Ifvalrefers to an element inside thisVectorandVectorneeds to reallocate,valwill dangle when the new object gets copy constructed.The copy assignment operator deletes
buffwithout checking whetherthis == &v. If that condition is true, it deletedv.buff(which it then will try to index in order to populate the newly allocatedbuff).- Also, that newly allocated
buffis too large by a factor ofsizeof(T).
- Also, that newly allocated
You might want to look into using
std::unique_ptr<T>forbuff. While this doesn't fix out of bounds memory access, it will help with some of the other issues.
Usage with standard algorithms
The name changes of
push_back,pop_back,cbeginandcendmake this container unusable for standard algorithms.beginandendshould provide aconstoverload, so they can be called on aconst Vector<T>&.Similarly,
cbeginandcendshould be declaredconst.Also,
push_front,pop_front,emplace,emplace_back,emplace_front,remove,insertand all the reverse iterator variants are missing.
Naming
- The style of names for member variables is inconsistent:
m_size/m_capacity,buffandLog. Choose one and stay consistent!
Class design
There is no way to handle types that have no default or no copy constructor. Please provide an overload on
push_back(T&&)to accept non-copyable types, andemplace_front/emplace_backmethods to constructTobjects for types that have neither a copy nor a move constructor. (Theemplace-type methods can also provide performance benefits by reducing copy/move constructions.)A
const Vector<T>&has no way to access elements inside it. Please provideconstoverloads forfront,backandoperator.Vector<T>::swapshould likely bepublic.Vector<T>isn't copyable unlessTitself is. Thus, the copy constructor and copy assignment operator should bedeleted for those cases.Why force the buffer to a size that is a power of 2? If I specify an exact size in the constructor (or via
reserve/resize), I'd expect theVectorto have exactly that size (and not waste additional memory).No member functions give any exception specification. This prevents certain kinds of optimization. Especially the move constructor, move assignment operator and destructor should be
noexcept.
Other issues
Why
reinterpret_cast<T*>(buff)?buffis already of typeT*.inlineactually doesn't do anything in this implementation.clear()doesn't call destructors for objects betweenm_sizeandm_capacity. (Then again, it doesn'tdelete buff, which would fix that.)Prefer
over()for initialization. While there is no instance of the most vexing parse in this code, it always helps to be mindful.As @TobySpeigh mentions, please put all code into the header file, as all code for templates needs to be accessible where instantiated. (Currently, one would have to use
#include "vector.cpp", which is surprising.)Iterators returned by
end()andcend()are expected to point one element after the last one (i.e.buff + m_size). Currently, they point wildly out of bounds (buff + (m_size - 1) * sizeof(T)).Sometimes
std::copyis used for copying, sometimes a rawforloop. This is inconsistent.In some of those cases, they could be replaced by a range-based
std::move(ifTis nothrow movable).In some cases, the last element isn't copied due to bound miscalculations.
Performance
No member function gives any exception specification. This might impede performance.
Why use floating point math for
Log? (And why useLogat all?)Try to
std::moveobjects if possible. This can be asserted with template meta programming.You might consider using
reallocfor reallocations ifTis trivially copyable.And as always, if performance is important, measure it! Doing premature optimization (especially at the cost of readability or extensibility) is bad.
Also, be sure your code is working correctly before trying to optimize. It doesn't matter if it is really fast if it produces the wrong result!
edited Jul 13 at 1:38
answered Jul 12 at 17:29
hoffmale
4,205630
4,205630
Many programmers prefer power-of-two heap allocations so as to minimize heap fragmentation. It also gets you O(log n) reallocation when repeatedly inserting.
â Davislor
Jul 12 at 22:30
1
@Davislor: Every constant growth factor provides $mathcalO(log n)$ reallocations upon insertion. And minimizing head fragmentation might be a good general goal, but not wasting memory if the user explicitly specifies their size requirements should be weighed higher IMHO.
â hoffmale
Jul 13 at 0:15
@hoffmale Every constant growth factor does, yes. Allocating only as much memory as you need, though, might need a billion allocations if you add a billion elements one-by-one. The counterargument to wasting memory was: if you do a lot of resizing or deallocation of small objects, you might in practice end up with lots of heap blocks that are too small to use, but that take a long time to search.
â Davislor
Jul 13 at 0:54
@hoffmale Added a section to my answer about that.
â Davislor
Jul 13 at 1:00
1
@Davislor Power of two heap allocations lead to the walking vector problem if you have one growing vector; no amount of previously freed vector buffers can fit your new memory request. Any sub power of 2 can avoid this, where after some number K iterations the previously deallocated K buffers can be used for the K+2nd exponential growth.
â Yakk
Jul 13 at 19:11
 |Â
show 5 more comments
Many programmers prefer power-of-two heap allocations so as to minimize heap fragmentation. It also gets you O(log n) reallocation when repeatedly inserting.
â Davislor
Jul 12 at 22:30
1
@Davislor: Every constant growth factor provides $mathcalO(log n)$ reallocations upon insertion. And minimizing head fragmentation might be a good general goal, but not wasting memory if the user explicitly specifies their size requirements should be weighed higher IMHO.
â hoffmale
Jul 13 at 0:15
@hoffmale Every constant growth factor does, yes. Allocating only as much memory as you need, though, might need a billion allocations if you add a billion elements one-by-one. The counterargument to wasting memory was: if you do a lot of resizing or deallocation of small objects, you might in practice end up with lots of heap blocks that are too small to use, but that take a long time to search.
â Davislor
Jul 13 at 0:54
@hoffmale Added a section to my answer about that.
â Davislor
Jul 13 at 1:00
1
@Davislor Power of two heap allocations lead to the walking vector problem if you have one growing vector; no amount of previously freed vector buffers can fit your new memory request. Any sub power of 2 can avoid this, where after some number K iterations the previously deallocated K buffers can be used for the K+2nd exponential growth.
â Yakk
Jul 13 at 19:11
Many programmers prefer power-of-two heap allocations so as to minimize heap fragmentation. It also gets you O(log n) reallocation when repeatedly inserting.
â Davislor
Jul 12 at 22:30
Many programmers prefer power-of-two heap allocations so as to minimize heap fragmentation. It also gets you O(log n) reallocation when repeatedly inserting.
â Davislor
Jul 12 at 22:30
1
1
@Davislor: Every constant growth factor provides $mathcalO(log n)$ reallocations upon insertion. And minimizing head fragmentation might be a good general goal, but not wasting memory if the user explicitly specifies their size requirements should be weighed higher IMHO.
â hoffmale
Jul 13 at 0:15
@Davislor: Every constant growth factor provides $mathcalO(log n)$ reallocations upon insertion. And minimizing head fragmentation might be a good general goal, but not wasting memory if the user explicitly specifies their size requirements should be weighed higher IMHO.
â hoffmale
Jul 13 at 0:15
@hoffmale Every constant growth factor does, yes. Allocating only as much memory as you need, though, might need a billion allocations if you add a billion elements one-by-one. The counterargument to wasting memory was: if you do a lot of resizing or deallocation of small objects, you might in practice end up with lots of heap blocks that are too small to use, but that take a long time to search.
â Davislor
Jul 13 at 0:54
@hoffmale Every constant growth factor does, yes. Allocating only as much memory as you need, though, might need a billion allocations if you add a billion elements one-by-one. The counterargument to wasting memory was: if you do a lot of resizing or deallocation of small objects, you might in practice end up with lots of heap blocks that are too small to use, but that take a long time to search.
â Davislor
Jul 13 at 0:54
@hoffmale Added a section to my answer about that.
â Davislor
Jul 13 at 1:00
@hoffmale Added a section to my answer about that.
â Davislor
Jul 13 at 1:00
1
1
@Davislor Power of two heap allocations lead to the walking vector problem if you have one growing vector; no amount of previously freed vector buffers can fit your new memory request. Any sub power of 2 can avoid this, where after some number K iterations the previously deallocated K buffers can be used for the K+2nd exponential growth.
â Yakk
Jul 13 at 19:11
@Davislor Power of two heap allocations lead to the walking vector problem if you have one growing vector; no amount of previously freed vector buffers can fit your new memory request. Any sub power of 2 can avoid this, where after some number K iterations the previously deallocated K buffers can be used for the K+2nd exponential growth.
â Yakk
Jul 13 at 19:11
 |Â
show 5 more comments
up vote
11
down vote
A few things you can improve upon:
Use more of <algorithm> and <memory>
Raw loops are verbose and unreadable. Use algorithms whenever you can -I've just noticed that you did it in some cases, but left the raw loops in others (cf reserve). You also seem not to know std::uninitialized_fill, which does exactly what you do manually in your third constructor, or std::destroy that's here to achieve what you do in clear (std::destroy_at is for individual elements, as in popBack).
Factorize constructors
Constructors can call other constructors. It avoids some copy-pasting. So, given:
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
you can have:
template <typename T>
Vector<T>::Vector(unsigned size, const T& value) : Vector(size)
std::copy(buffer, buffer+size, value);
Simplify your destructor
You don't need to reset all member variables in your destructor, since you can't use it after anyway.
add a comment |Â
up vote
11
down vote
A few things you can improve upon:
Use more of <algorithm> and <memory>
Raw loops are verbose and unreadable. Use algorithms whenever you can -I've just noticed that you did it in some cases, but left the raw loops in others (cf reserve). You also seem not to know std::uninitialized_fill, which does exactly what you do manually in your third constructor, or std::destroy that's here to achieve what you do in clear (std::destroy_at is for individual elements, as in popBack).
Factorize constructors
Constructors can call other constructors. It avoids some copy-pasting. So, given:
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
you can have:
template <typename T>
Vector<T>::Vector(unsigned size, const T& value) : Vector(size)
std::copy(buffer, buffer+size, value);
Simplify your destructor
You don't need to reset all member variables in your destructor, since you can't use it after anyway.
add a comment |Â
up vote
11
down vote
up vote
11
down vote
A few things you can improve upon:
Use more of <algorithm> and <memory>
Raw loops are verbose and unreadable. Use algorithms whenever you can -I've just noticed that you did it in some cases, but left the raw loops in others (cf reserve). You also seem not to know std::uninitialized_fill, which does exactly what you do manually in your third constructor, or std::destroy that's here to achieve what you do in clear (std::destroy_at is for individual elements, as in popBack).
Factorize constructors
Constructors can call other constructors. It avoids some copy-pasting. So, given:
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
you can have:
template <typename T>
Vector<T>::Vector(unsigned size, const T& value) : Vector(size)
std::copy(buffer, buffer+size, value);
Simplify your destructor
You don't need to reset all member variables in your destructor, since you can't use it after anyway.
A few things you can improve upon:
Use more of <algorithm> and <memory>
Raw loops are verbose and unreadable. Use algorithms whenever you can -I've just noticed that you did it in some cases, but left the raw loops in others (cf reserve). You also seem not to know std::uninitialized_fill, which does exactly what you do manually in your third constructor, or std::destroy that's here to achieve what you do in clear (std::destroy_at is for individual elements, as in popBack).
Factorize constructors
Constructors can call other constructors. It avoids some copy-pasting. So, given:
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
you can have:
template <typename T>
Vector<T>::Vector(unsigned size, const T& value) : Vector(size)
std::copy(buffer, buffer+size, value);
Simplify your destructor
You don't need to reset all member variables in your destructor, since you can't use it after anyway.
answered Jul 12 at 14:02
papagaga
2,444115
2,444115
add a comment |Â
add a comment |Â
up vote
10
down vote
Use the well-known names for methods
c_begin and c_end are so close to the standard-container cbegin and cend that users will forever curse you. Likewise for pushBack and popBack (instead of the usual push_back and pop_back).
Use a consistent naming scheme within the class
Members seem to have a mix of naming schemes: buff, m_size, Log. Pick one and stick with it.
Avoid unnecessary code
In the destructor, we have:
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
These statements are useless clutter, as the object is being destroyed, and the members will not be accessible after this. A good compiler may elide the code for you, but it can't address the bigger problem, that there's more lines for the human reader to understand.
Similarly, in the move-constructor, we perform most of the destructor actions before calling swap(). It's generally expected that a moved-from object should soon be going out of scope, and perfectly normal to just swap the contents on the assumption that this tidy-up will occur then.
The clear() method leaks memory
We set buff to a null pointer without deleting its resource - there's a missing delete buff there. It's really worth running the unit tests under Valgrind occasionally to catch stuff like that.
Don't delete contents on assignment...
...until you have checked for self-assignment. That's one mistake that copy-and-swap will save you from.
It will help performance if you also check whether the current capacity is already suitable for the new contents. If it's at least the required size (and not too much bigger - e.g. one or two reallocation steps at most), then you can reduce the work of new and delete by simply re-using the storage you allocated.
Use the correct names of functions from <cmath>
It appears that your compiler exercises its right to define names in the global namespace as well as in std. But it's not portable to rely on that, so call std::log and std::ceil by their full names.
Use a single file
The method definitions need to be visible to the users (as they are templates), so they should be in the header file with the declarations.
Actually, there is aclear()member function...
â hoffmale
Jul 12 at 17:36
Thanks @hoffmale - now I see theclear()member, I see a bug in it, so I've edited accordingly.
â Toby Speight
Jul 12 at 18:11
add a comment |Â
up vote
10
down vote
Use the well-known names for methods
c_begin and c_end are so close to the standard-container cbegin and cend that users will forever curse you. Likewise for pushBack and popBack (instead of the usual push_back and pop_back).
Use a consistent naming scheme within the class
Members seem to have a mix of naming schemes: buff, m_size, Log. Pick one and stick with it.
Avoid unnecessary code
In the destructor, we have:
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
These statements are useless clutter, as the object is being destroyed, and the members will not be accessible after this. A good compiler may elide the code for you, but it can't address the bigger problem, that there's more lines for the human reader to understand.
Similarly, in the move-constructor, we perform most of the destructor actions before calling swap(). It's generally expected that a moved-from object should soon be going out of scope, and perfectly normal to just swap the contents on the assumption that this tidy-up will occur then.
The clear() method leaks memory
We set buff to a null pointer without deleting its resource - there's a missing delete buff there. It's really worth running the unit tests under Valgrind occasionally to catch stuff like that.
Don't delete contents on assignment...
...until you have checked for self-assignment. That's one mistake that copy-and-swap will save you from.
It will help performance if you also check whether the current capacity is already suitable for the new contents. If it's at least the required size (and not too much bigger - e.g. one or two reallocation steps at most), then you can reduce the work of new and delete by simply re-using the storage you allocated.
Use the correct names of functions from <cmath>
It appears that your compiler exercises its right to define names in the global namespace as well as in std. But it's not portable to rely on that, so call std::log and std::ceil by their full names.
Use a single file
The method definitions need to be visible to the users (as they are templates), so they should be in the header file with the declarations.
Actually, there is aclear()member function...
â hoffmale
Jul 12 at 17:36
Thanks @hoffmale - now I see theclear()member, I see a bug in it, so I've edited accordingly.
â Toby Speight
Jul 12 at 18:11
add a comment |Â
up vote
10
down vote
up vote
10
down vote
Use the well-known names for methods
c_begin and c_end are so close to the standard-container cbegin and cend that users will forever curse you. Likewise for pushBack and popBack (instead of the usual push_back and pop_back).
Use a consistent naming scheme within the class
Members seem to have a mix of naming schemes: buff, m_size, Log. Pick one and stick with it.
Avoid unnecessary code
In the destructor, we have:
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
These statements are useless clutter, as the object is being destroyed, and the members will not be accessible after this. A good compiler may elide the code for you, but it can't address the bigger problem, that there's more lines for the human reader to understand.
Similarly, in the move-constructor, we perform most of the destructor actions before calling swap(). It's generally expected that a moved-from object should soon be going out of scope, and perfectly normal to just swap the contents on the assumption that this tidy-up will occur then.
The clear() method leaks memory
We set buff to a null pointer without deleting its resource - there's a missing delete buff there. It's really worth running the unit tests under Valgrind occasionally to catch stuff like that.
Don't delete contents on assignment...
...until you have checked for self-assignment. That's one mistake that copy-and-swap will save you from.
It will help performance if you also check whether the current capacity is already suitable for the new contents. If it's at least the required size (and not too much bigger - e.g. one or two reallocation steps at most), then you can reduce the work of new and delete by simply re-using the storage you allocated.
Use the correct names of functions from <cmath>
It appears that your compiler exercises its right to define names in the global namespace as well as in std. But it's not portable to rely on that, so call std::log and std::ceil by their full names.
Use a single file
The method definitions need to be visible to the users (as they are templates), so they should be in the header file with the declarations.
Use the well-known names for methods
c_begin and c_end are so close to the standard-container cbegin and cend that users will forever curse you. Likewise for pushBack and popBack (instead of the usual push_back and pop_back).
Use a consistent naming scheme within the class
Members seem to have a mix of naming schemes: buff, m_size, Log. Pick one and stick with it.
Avoid unnecessary code
In the destructor, we have:
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
These statements are useless clutter, as the object is being destroyed, and the members will not be accessible after this. A good compiler may elide the code for you, but it can't address the bigger problem, that there's more lines for the human reader to understand.
Similarly, in the move-constructor, we perform most of the destructor actions before calling swap(). It's generally expected that a moved-from object should soon be going out of scope, and perfectly normal to just swap the contents on the assumption that this tidy-up will occur then.
The clear() method leaks memory
We set buff to a null pointer without deleting its resource - there's a missing delete buff there. It's really worth running the unit tests under Valgrind occasionally to catch stuff like that.
Don't delete contents on assignment...
...until you have checked for self-assignment. That's one mistake that copy-and-swap will save you from.
It will help performance if you also check whether the current capacity is already suitable for the new contents. If it's at least the required size (and not too much bigger - e.g. one or two reallocation steps at most), then you can reduce the work of new and delete by simply re-using the storage you allocated.
Use the correct names of functions from <cmath>
It appears that your compiler exercises its right to define names in the global namespace as well as in std. But it's not portable to rely on that, so call std::log and std::ceil by their full names.
Use a single file
The method definitions need to be visible to the users (as they are templates), so they should be in the header file with the declarations.
edited Jul 12 at 18:12
hoffmale
4,205630
4,205630
answered Jul 12 at 14:56
Toby Speight
17.2k13486
17.2k13486
Actually, there is aclear()member function...
â hoffmale
Jul 12 at 17:36
Thanks @hoffmale - now I see theclear()member, I see a bug in it, so I've edited accordingly.
â Toby Speight
Jul 12 at 18:11
add a comment |Â
Actually, there is aclear()member function...
â hoffmale
Jul 12 at 17:36
Thanks @hoffmale - now I see theclear()member, I see a bug in it, so I've edited accordingly.
â Toby Speight
Jul 12 at 18:11
Actually, there is a
clear() member function...â hoffmale
Jul 12 at 17:36
Actually, there is a
clear() member function...â hoffmale
Jul 12 at 17:36
Thanks @hoffmale - now I see the
clear() member, I see a bug in it, so I've edited accordingly.â Toby Speight
Jul 12 at 18:11
Thanks @hoffmale - now I see the
clear() member, I see a bug in it, so I've edited accordingly.â Toby Speight
Jul 12 at 18:11
add a comment |Â
up vote
9
down vote
There is a bug in pushBack, you need to take into account the case where you call it on an existing member of the array.
For example in the code
Vector<int> v(1,0);
v.pushBack(v[0]);
pushBack will first resize the array, invalidating the reference to v[0]. It will then attempt to insert that invalid reference in the new array.
Wow, totally overlooked that one with all the other memory management issues!
â hoffmale
Jul 12 at 17:51
Actually, this behavior is undefined in this specific case.Vector<int> v(1, 0)initializesm_capacitywrong, thusv.pushBack(v[0])might not actually reallocate.
â hoffmale
Jul 12 at 18:11
add a comment |Â
up vote
9
down vote
There is a bug in pushBack, you need to take into account the case where you call it on an existing member of the array.
For example in the code
Vector<int> v(1,0);
v.pushBack(v[0]);
pushBack will first resize the array, invalidating the reference to v[0]. It will then attempt to insert that invalid reference in the new array.
Wow, totally overlooked that one with all the other memory management issues!
â hoffmale
Jul 12 at 17:51
Actually, this behavior is undefined in this specific case.Vector<int> v(1, 0)initializesm_capacitywrong, thusv.pushBack(v[0])might not actually reallocate.
â hoffmale
Jul 12 at 18:11
add a comment |Â
up vote
9
down vote
up vote
9
down vote
There is a bug in pushBack, you need to take into account the case where you call it on an existing member of the array.
For example in the code
Vector<int> v(1,0);
v.pushBack(v[0]);
pushBack will first resize the array, invalidating the reference to v[0]. It will then attempt to insert that invalid reference in the new array.
There is a bug in pushBack, you need to take into account the case where you call it on an existing member of the array.
For example in the code
Vector<int> v(1,0);
v.pushBack(v[0]);
pushBack will first resize the array, invalidating the reference to v[0]. It will then attempt to insert that invalid reference in the new array.
answered Jul 12 at 17:26
David Brown
1913
1913
Wow, totally overlooked that one with all the other memory management issues!
â hoffmale
Jul 12 at 17:51
Actually, this behavior is undefined in this specific case.Vector<int> v(1, 0)initializesm_capacitywrong, thusv.pushBack(v[0])might not actually reallocate.
â hoffmale
Jul 12 at 18:11
add a comment |Â
Wow, totally overlooked that one with all the other memory management issues!
â hoffmale
Jul 12 at 17:51
Actually, this behavior is undefined in this specific case.Vector<int> v(1, 0)initializesm_capacitywrong, thusv.pushBack(v[0])might not actually reallocate.
â hoffmale
Jul 12 at 18:11
Wow, totally overlooked that one with all the other memory management issues!
â hoffmale
Jul 12 at 17:51
Wow, totally overlooked that one with all the other memory management issues!
â hoffmale
Jul 12 at 17:51
Actually, this behavior is undefined in this specific case.
Vector<int> v(1, 0) initializes m_capacity wrong, thus v.pushBack(v[0]) might not actually reallocate.â hoffmale
Jul 12 at 18:11
Actually, this behavior is undefined in this specific case.
Vector<int> v(1, 0) initializes m_capacity wrong, thus v.pushBack(v[0]) might not actually reallocate.â hoffmale
Jul 12 at 18:11
add a comment |Â
up vote
8
down vote
Problems
Your main issue is that your usage of placement new (and manual destructor call) are incorrect and undefined behavior results because the lifespan of objects is inconsistent.
Code Review
Seems a bit likely to clash with other people
#ifndef VECTOR_H
#define VECTOR_H
You have to make these unique include your namespace into the guard. Talking about namespace; add a namespace around your code.
Use of inline is discouraged.
inline unsigned int capacity() const return m_capacity;
inline unsigned int size() const return m_size;
inline bool empty() const return m_size == 0;
inline iterator begin() return buff;
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_begin() return buff;
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
In this context it does not add any real meaning. It was supposed to be a hint to the compiler to inline the code. But it was long since shown that humans are very bad at making this choice and all compilers ignore the hint and only inline when they determine it is useful.
These look wrong.
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
There is no need to add sizeof(T) in those expressions. When you increment a pointer you are adding by the size of the object that it points at.
Seems like you need const versions of these two (in addition to the non const versions).
T& front();
T& back();
T& operator (unsigned int index);
You have move constructors. Why not move insertion.
void pushBack(const T& val);
Why not just use a default parameter in the next constructor to implement this version of the constructor?
template<typename T>
Vector<T>::Vector() :
m_capacity(0),
m_size(0),
Log(0),
buff(nullptr)
The order of the initialization list is not relevant.
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
The members are initialized in the order they are declared in class declaration. Changing the order in the initializer list will not change that order. If you turn on your compiler warnings it will tell you this.
As a result these members are not all initialized correctly as some depend on values that have not been initialized yet.
The actual order that will be done is:
m_capacity(1 << Log), // Notice here Log is used
// But has not been initialized.
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
buff(size ? new T[m_capacity] : nullptr)
Incorrect usage of placement new.
buff(size ? new T[m_capacity] : nullptr) // You are calling the constructor of `T` for every object in this array
....
new(buffer + i) T(initial); // You are over writing an object that has had its constructor called.
Placement new can only be used on RAW memory. You are using it here on an already live object (an object that has had its constructor already called). This is undefined behavior.
It would have been more correct to use the assignment operator.
It is a good idea to mark the move constructor as noexcept.
template<typename T>
Vector<T>::Vector(Vector<T> && v)
: m_size(0),
m_capacity(0),
buff(nullptr),
Log(0)
swap(v);
This allows for certain optimizations by the compiler. Since the only call is to swap() this should be true (traditionally swap is also noexcept).
The Assignment operator does not provide the strong exception guarantee. Also it is not self assignment safe. If you assign this object to itself it will have undefined behavior.
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> const& v)
// //not strong exception, but will write a wrapper that makes it strong exception; lowest level -> fastest
delete buff;
buff = nullptr;
buff = v.m_capacity ? new T[v.m_capacity * sizeof(T)] : nullptr;
m_size = v.m_size;
m_capacity = v.m_capacity;
Log = v.Log;
std::copy(v.buff, v.buff + m_size-1, buff);
return *this;
I see you notice that in your comments. It is trivial to make this exception safe by using the copy and swap idiom (and it has no more overhead than doing it manually). Also the copy and swap idiom is self assignment safe.
Again move operator. Usually mark as noexcept. Unfortunately as you call delete you can't do that!
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> && v)
delete buff; //prep this
m_size = 0;
buff = nullptr;
m_capacity = 0;
Log = 0;
swap(v);
return *this;
There is no need to do the delete here. Simply do the swap. This will allow you to mark the method noexcept. After you have done the swap call clear on the RHS to force the objects in the vector to be destroyed (to mimic the requirements on the std::vector). Let the destructor of the RHS clean up the memory for the vector.
It also adds the opportunity to optimize the usage (as the storage can potentially be re-used).
Extra work being done.
template<typename T>
Vector<T>::~Vector()
delete buff;
// Everything below this point is a waste of time.
// Don't do it.
// Also assigning null to the pointer can potentially hide errors. When you are debugging.
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
This call to the destructor will get you into trouble.
template<typename T>
void Vector<T>::popBack()
(reinterpret_cast<T*>(buff)[m_size-- - 1]).~T();
The trouble is that buff is a pointer to T so when you call delete in the destructor it is going call the destructor for T. By calling it here you have ended the objects life span so calling delete in the destructor is undefined behavior.
I wrote 5 articles about building a vector.
Vector - Resource Management Allocation
Vector - Resource Management Copy Swap
Vector - Resize
Vector - Simple Optimizations
Vector - The Other Stuff
add a comment |Â
up vote
8
down vote
Problems
Your main issue is that your usage of placement new (and manual destructor call) are incorrect and undefined behavior results because the lifespan of objects is inconsistent.
Code Review
Seems a bit likely to clash with other people
#ifndef VECTOR_H
#define VECTOR_H
You have to make these unique include your namespace into the guard. Talking about namespace; add a namespace around your code.
Use of inline is discouraged.
inline unsigned int capacity() const return m_capacity;
inline unsigned int size() const return m_size;
inline bool empty() const return m_size == 0;
inline iterator begin() return buff;
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_begin() return buff;
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
In this context it does not add any real meaning. It was supposed to be a hint to the compiler to inline the code. But it was long since shown that humans are very bad at making this choice and all compilers ignore the hint and only inline when they determine it is useful.
These look wrong.
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
There is no need to add sizeof(T) in those expressions. When you increment a pointer you are adding by the size of the object that it points at.
Seems like you need const versions of these two (in addition to the non const versions).
T& front();
T& back();
T& operator (unsigned int index);
You have move constructors. Why not move insertion.
void pushBack(const T& val);
Why not just use a default parameter in the next constructor to implement this version of the constructor?
template<typename T>
Vector<T>::Vector() :
m_capacity(0),
m_size(0),
Log(0),
buff(nullptr)
The order of the initialization list is not relevant.
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
The members are initialized in the order they are declared in class declaration. Changing the order in the initializer list will not change that order. If you turn on your compiler warnings it will tell you this.
As a result these members are not all initialized correctly as some depend on values that have not been initialized yet.
The actual order that will be done is:
m_capacity(1 << Log), // Notice here Log is used
// But has not been initialized.
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
buff(size ? new T[m_capacity] : nullptr)
Incorrect usage of placement new.
buff(size ? new T[m_capacity] : nullptr) // You are calling the constructor of `T` for every object in this array
....
new(buffer + i) T(initial); // You are over writing an object that has had its constructor called.
Placement new can only be used on RAW memory. You are using it here on an already live object (an object that has had its constructor already called). This is undefined behavior.
It would have been more correct to use the assignment operator.
It is a good idea to mark the move constructor as noexcept.
template<typename T>
Vector<T>::Vector(Vector<T> && v)
: m_size(0),
m_capacity(0),
buff(nullptr),
Log(0)
swap(v);
This allows for certain optimizations by the compiler. Since the only call is to swap() this should be true (traditionally swap is also noexcept).
The Assignment operator does not provide the strong exception guarantee. Also it is not self assignment safe. If you assign this object to itself it will have undefined behavior.
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> const& v)
// //not strong exception, but will write a wrapper that makes it strong exception; lowest level -> fastest
delete buff;
buff = nullptr;
buff = v.m_capacity ? new T[v.m_capacity * sizeof(T)] : nullptr;
m_size = v.m_size;
m_capacity = v.m_capacity;
Log = v.Log;
std::copy(v.buff, v.buff + m_size-1, buff);
return *this;
I see you notice that in your comments. It is trivial to make this exception safe by using the copy and swap idiom (and it has no more overhead than doing it manually). Also the copy and swap idiom is self assignment safe.
Again move operator. Usually mark as noexcept. Unfortunately as you call delete you can't do that!
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> && v)
delete buff; //prep this
m_size = 0;
buff = nullptr;
m_capacity = 0;
Log = 0;
swap(v);
return *this;
There is no need to do the delete here. Simply do the swap. This will allow you to mark the method noexcept. After you have done the swap call clear on the RHS to force the objects in the vector to be destroyed (to mimic the requirements on the std::vector). Let the destructor of the RHS clean up the memory for the vector.
It also adds the opportunity to optimize the usage (as the storage can potentially be re-used).
Extra work being done.
template<typename T>
Vector<T>::~Vector()
delete buff;
// Everything below this point is a waste of time.
// Don't do it.
// Also assigning null to the pointer can potentially hide errors. When you are debugging.
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
This call to the destructor will get you into trouble.
template<typename T>
void Vector<T>::popBack()
(reinterpret_cast<T*>(buff)[m_size-- - 1]).~T();
The trouble is that buff is a pointer to T so when you call delete in the destructor it is going call the destructor for T. By calling it here you have ended the objects life span so calling delete in the destructor is undefined behavior.
I wrote 5 articles about building a vector.
Vector - Resource Management Allocation
Vector - Resource Management Copy Swap
Vector - Resize
Vector - Simple Optimizations
Vector - The Other Stuff
add a comment |Â
up vote
8
down vote
up vote
8
down vote
Problems
Your main issue is that your usage of placement new (and manual destructor call) are incorrect and undefined behavior results because the lifespan of objects is inconsistent.
Code Review
Seems a bit likely to clash with other people
#ifndef VECTOR_H
#define VECTOR_H
You have to make these unique include your namespace into the guard. Talking about namespace; add a namespace around your code.
Use of inline is discouraged.
inline unsigned int capacity() const return m_capacity;
inline unsigned int size() const return m_size;
inline bool empty() const return m_size == 0;
inline iterator begin() return buff;
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_begin() return buff;
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
In this context it does not add any real meaning. It was supposed to be a hint to the compiler to inline the code. But it was long since shown that humans are very bad at making this choice and all compilers ignore the hint and only inline when they determine it is useful.
These look wrong.
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
There is no need to add sizeof(T) in those expressions. When you increment a pointer you are adding by the size of the object that it points at.
Seems like you need const versions of these two (in addition to the non const versions).
T& front();
T& back();
T& operator (unsigned int index);
You have move constructors. Why not move insertion.
void pushBack(const T& val);
Why not just use a default parameter in the next constructor to implement this version of the constructor?
template<typename T>
Vector<T>::Vector() :
m_capacity(0),
m_size(0),
Log(0),
buff(nullptr)
The order of the initialization list is not relevant.
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
The members are initialized in the order they are declared in class declaration. Changing the order in the initializer list will not change that order. If you turn on your compiler warnings it will tell you this.
As a result these members are not all initialized correctly as some depend on values that have not been initialized yet.
The actual order that will be done is:
m_capacity(1 << Log), // Notice here Log is used
// But has not been initialized.
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
buff(size ? new T[m_capacity] : nullptr)
Incorrect usage of placement new.
buff(size ? new T[m_capacity] : nullptr) // You are calling the constructor of `T` for every object in this array
....
new(buffer + i) T(initial); // You are over writing an object that has had its constructor called.
Placement new can only be used on RAW memory. You are using it here on an already live object (an object that has had its constructor already called). This is undefined behavior.
It would have been more correct to use the assignment operator.
It is a good idea to mark the move constructor as noexcept.
template<typename T>
Vector<T>::Vector(Vector<T> && v)
: m_size(0),
m_capacity(0),
buff(nullptr),
Log(0)
swap(v);
This allows for certain optimizations by the compiler. Since the only call is to swap() this should be true (traditionally swap is also noexcept).
The Assignment operator does not provide the strong exception guarantee. Also it is not self assignment safe. If you assign this object to itself it will have undefined behavior.
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> const& v)
// //not strong exception, but will write a wrapper that makes it strong exception; lowest level -> fastest
delete buff;
buff = nullptr;
buff = v.m_capacity ? new T[v.m_capacity * sizeof(T)] : nullptr;
m_size = v.m_size;
m_capacity = v.m_capacity;
Log = v.Log;
std::copy(v.buff, v.buff + m_size-1, buff);
return *this;
I see you notice that in your comments. It is trivial to make this exception safe by using the copy and swap idiom (and it has no more overhead than doing it manually). Also the copy and swap idiom is self assignment safe.
Again move operator. Usually mark as noexcept. Unfortunately as you call delete you can't do that!
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> && v)
delete buff; //prep this
m_size = 0;
buff = nullptr;
m_capacity = 0;
Log = 0;
swap(v);
return *this;
There is no need to do the delete here. Simply do the swap. This will allow you to mark the method noexcept. After you have done the swap call clear on the RHS to force the objects in the vector to be destroyed (to mimic the requirements on the std::vector). Let the destructor of the RHS clean up the memory for the vector.
It also adds the opportunity to optimize the usage (as the storage can potentially be re-used).
Extra work being done.
template<typename T>
Vector<T>::~Vector()
delete buff;
// Everything below this point is a waste of time.
// Don't do it.
// Also assigning null to the pointer can potentially hide errors. When you are debugging.
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
This call to the destructor will get you into trouble.
template<typename T>
void Vector<T>::popBack()
(reinterpret_cast<T*>(buff)[m_size-- - 1]).~T();
The trouble is that buff is a pointer to T so when you call delete in the destructor it is going call the destructor for T. By calling it here you have ended the objects life span so calling delete in the destructor is undefined behavior.
I wrote 5 articles about building a vector.
Vector - Resource Management Allocation
Vector - Resource Management Copy Swap
Vector - Resize
Vector - Simple Optimizations
Vector - The Other Stuff
Problems
Your main issue is that your usage of placement new (and manual destructor call) are incorrect and undefined behavior results because the lifespan of objects is inconsistent.
Code Review
Seems a bit likely to clash with other people
#ifndef VECTOR_H
#define VECTOR_H
You have to make these unique include your namespace into the guard. Talking about namespace; add a namespace around your code.
Use of inline is discouraged.
inline unsigned int capacity() const return m_capacity;
inline unsigned int size() const return m_size;
inline bool empty() const return m_size == 0;
inline iterator begin() return buff;
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_begin() return buff;
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
In this context it does not add any real meaning. It was supposed to be a hint to the compiler to inline the code. But it was long since shown that humans are very bad at making this choice and all compilers ignore the hint and only inline when they determine it is useful.
These look wrong.
inline iterator end() return buff + (m_size - 1) * sizeof(T);
inline const_iterator c_end() return buff + (m_size - 1) * sizeof(T);
There is no need to add sizeof(T) in those expressions. When you increment a pointer you are adding by the size of the object that it points at.
Seems like you need const versions of these two (in addition to the non const versions).
T& front();
T& back();
T& operator (unsigned int index);
You have move constructors. Why not move insertion.
void pushBack(const T& val);
Why not just use a default parameter in the next constructor to implement this version of the constructor?
template<typename T>
Vector<T>::Vector() :
m_capacity(0),
m_size(0),
Log(0),
buff(nullptr)
The order of the initialization list is not relevant.
template<typename T>
Vector<T>::Vector(unsigned int size) : //an argument of 0 leads to a capacity value of 1, distinct from the default ctor
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
m_capacity(1 << Log),
buff(size ? new T[m_capacity] : nullptr)
The members are initialized in the order they are declared in class declaration. Changing the order in the initializer list will not change that order. If you turn on your compiler warnings it will tell you this.
As a result these members are not all initialized correctly as some depend on values that have not been initialized yet.
The actual order that will be done is:
m_capacity(1 << Log), // Notice here Log is used
// But has not been initialized.
m_size(size),
Log(ceil(log((double)size) / log(2.0))),
buff(size ? new T[m_capacity] : nullptr)
Incorrect usage of placement new.
buff(size ? new T[m_capacity] : nullptr) // You are calling the constructor of `T` for every object in this array
....
new(buffer + i) T(initial); // You are over writing an object that has had its constructor called.
Placement new can only be used on RAW memory. You are using it here on an already live object (an object that has had its constructor already called). This is undefined behavior.
It would have been more correct to use the assignment operator.
It is a good idea to mark the move constructor as noexcept.
template<typename T>
Vector<T>::Vector(Vector<T> && v)
: m_size(0),
m_capacity(0),
buff(nullptr),
Log(0)
swap(v);
This allows for certain optimizations by the compiler. Since the only call is to swap() this should be true (traditionally swap is also noexcept).
The Assignment operator does not provide the strong exception guarantee. Also it is not self assignment safe. If you assign this object to itself it will have undefined behavior.
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> const& v)
// //not strong exception, but will write a wrapper that makes it strong exception; lowest level -> fastest
delete buff;
buff = nullptr;
buff = v.m_capacity ? new T[v.m_capacity * sizeof(T)] : nullptr;
m_size = v.m_size;
m_capacity = v.m_capacity;
Log = v.Log;
std::copy(v.buff, v.buff + m_size-1, buff);
return *this;
I see you notice that in your comments. It is trivial to make this exception safe by using the copy and swap idiom (and it has no more overhead than doing it manually). Also the copy and swap idiom is self assignment safe.
Again move operator. Usually mark as noexcept. Unfortunately as you call delete you can't do that!
template<typename T>
Vector<T>& Vector<T>::operator =(Vector<T> && v)
delete buff; //prep this
m_size = 0;
buff = nullptr;
m_capacity = 0;
Log = 0;
swap(v);
return *this;
There is no need to do the delete here. Simply do the swap. This will allow you to mark the method noexcept. After you have done the swap call clear on the RHS to force the objects in the vector to be destroyed (to mimic the requirements on the std::vector). Let the destructor of the RHS clean up the memory for the vector.
It also adds the opportunity to optimize the usage (as the storage can potentially be re-used).
Extra work being done.
template<typename T>
Vector<T>::~Vector()
delete buff;
// Everything below this point is a waste of time.
// Don't do it.
// Also assigning null to the pointer can potentially hide errors. When you are debugging.
buff = nullptr;
m_size = 0;
m_capacity = 0;
Log = 0;
This call to the destructor will get you into trouble.
template<typename T>
void Vector<T>::popBack()
(reinterpret_cast<T*>(buff)[m_size-- - 1]).~T();
The trouble is that buff is a pointer to T so when you call delete in the destructor it is going call the destructor for T. By calling it here you have ended the objects life span so calling delete in the destructor is undefined behavior.
I wrote 5 articles about building a vector.
Vector - Resource Management Allocation
Vector - Resource Management Copy Swap
Vector - Resize
Vector - Simple Optimizations
Vector - The Other Stuff
edited Jul 13 at 0:10
answered Jul 12 at 20:18
Martin York
70.7k481244
70.7k481244
add a comment |Â
add a comment |Â
up vote
7
down vote
The Type of Your Size Parameters
Currently, you have them as unsigned int, but this is often different from size_t and ptrdiff_t. The most common example in 2018 is that most 64-bit compilers define unsigned int as 32 bits wide, which limits your vectors to 4GiB. It's also possible to imagine implementations with 16- or 24-bit size_t but a wider ALU (a 16-bit Windows or DOS program running on an 80386 or better?) The STL version uses size_t, which is 32 bits wide on 32-bit platforms and 64 bits wide on 64-bit platforms.
You will also often find some people who prefer signed to unsigned indices, on the grounds that it's very easy for arithmetic on indices to give you a negative number, and interpreting that as a very large unsigned number will lead to logic errors. This has a portability issue of its own, in that unsigned overflow and underflow are well-defined but signed overflow and underflow are not, but nearly all real-world implementations use twoâÂÂs-complement math. If you want to use signed indices, the type for that is ptrdiff_t, which is the type of the result of subtracting one pointer from another.
MicrosoftâÂÂs preferred solution is rsize_t, an optional annex to the C language standard. This is an unsigned quantity whose upper bit flags it as invalid (that is, a size_t that the library is meant to rigorously check for overflow).
Allocation Size
You choose to allocate blocks as powers of two. Elsewhere in the comments, when someone asked if it wouldnâÂÂt be better to allocate the exact amount of memory requested, I explain the arguments in favor of the power-of-two approach.
However, adding a .shrink_to_fit() method gives the client code the ability to decide that wasted memory is a bigger concern for that application than heap fragmentation. If the application doesnâÂÂt call that, they get the default behavior.
add a comment |Â
up vote
7
down vote
The Type of Your Size Parameters
Currently, you have them as unsigned int, but this is often different from size_t and ptrdiff_t. The most common example in 2018 is that most 64-bit compilers define unsigned int as 32 bits wide, which limits your vectors to 4GiB. It's also possible to imagine implementations with 16- or 24-bit size_t but a wider ALU (a 16-bit Windows or DOS program running on an 80386 or better?) The STL version uses size_t, which is 32 bits wide on 32-bit platforms and 64 bits wide on 64-bit platforms.
You will also often find some people who prefer signed to unsigned indices, on the grounds that it's very easy for arithmetic on indices to give you a negative number, and interpreting that as a very large unsigned number will lead to logic errors. This has a portability issue of its own, in that unsigned overflow and underflow are well-defined but signed overflow and underflow are not, but nearly all real-world implementations use twoâÂÂs-complement math. If you want to use signed indices, the type for that is ptrdiff_t, which is the type of the result of subtracting one pointer from another.
MicrosoftâÂÂs preferred solution is rsize_t, an optional annex to the C language standard. This is an unsigned quantity whose upper bit flags it as invalid (that is, a size_t that the library is meant to rigorously check for overflow).
Allocation Size
You choose to allocate blocks as powers of two. Elsewhere in the comments, when someone asked if it wouldnâÂÂt be better to allocate the exact amount of memory requested, I explain the arguments in favor of the power-of-two approach.
However, adding a .shrink_to_fit() method gives the client code the ability to decide that wasted memory is a bigger concern for that application than heap fragmentation. If the application doesnâÂÂt call that, they get the default behavior.
add a comment |Â
up vote
7
down vote
up vote
7
down vote
The Type of Your Size Parameters
Currently, you have them as unsigned int, but this is often different from size_t and ptrdiff_t. The most common example in 2018 is that most 64-bit compilers define unsigned int as 32 bits wide, which limits your vectors to 4GiB. It's also possible to imagine implementations with 16- or 24-bit size_t but a wider ALU (a 16-bit Windows or DOS program running on an 80386 or better?) The STL version uses size_t, which is 32 bits wide on 32-bit platforms and 64 bits wide on 64-bit platforms.
You will also often find some people who prefer signed to unsigned indices, on the grounds that it's very easy for arithmetic on indices to give you a negative number, and interpreting that as a very large unsigned number will lead to logic errors. This has a portability issue of its own, in that unsigned overflow and underflow are well-defined but signed overflow and underflow are not, but nearly all real-world implementations use twoâÂÂs-complement math. If you want to use signed indices, the type for that is ptrdiff_t, which is the type of the result of subtracting one pointer from another.
MicrosoftâÂÂs preferred solution is rsize_t, an optional annex to the C language standard. This is an unsigned quantity whose upper bit flags it as invalid (that is, a size_t that the library is meant to rigorously check for overflow).
Allocation Size
You choose to allocate blocks as powers of two. Elsewhere in the comments, when someone asked if it wouldnâÂÂt be better to allocate the exact amount of memory requested, I explain the arguments in favor of the power-of-two approach.
However, adding a .shrink_to_fit() method gives the client code the ability to decide that wasted memory is a bigger concern for that application than heap fragmentation. If the application doesnâÂÂt call that, they get the default behavior.
The Type of Your Size Parameters
Currently, you have them as unsigned int, but this is often different from size_t and ptrdiff_t. The most common example in 2018 is that most 64-bit compilers define unsigned int as 32 bits wide, which limits your vectors to 4GiB. It's also possible to imagine implementations with 16- or 24-bit size_t but a wider ALU (a 16-bit Windows or DOS program running on an 80386 or better?) The STL version uses size_t, which is 32 bits wide on 32-bit platforms and 64 bits wide on 64-bit platforms.
You will also often find some people who prefer signed to unsigned indices, on the grounds that it's very easy for arithmetic on indices to give you a negative number, and interpreting that as a very large unsigned number will lead to logic errors. This has a portability issue of its own, in that unsigned overflow and underflow are well-defined but signed overflow and underflow are not, but nearly all real-world implementations use twoâÂÂs-complement math. If you want to use signed indices, the type for that is ptrdiff_t, which is the type of the result of subtracting one pointer from another.
MicrosoftâÂÂs preferred solution is rsize_t, an optional annex to the C language standard. This is an unsigned quantity whose upper bit flags it as invalid (that is, a size_t that the library is meant to rigorously check for overflow).
Allocation Size
You choose to allocate blocks as powers of two. Elsewhere in the comments, when someone asked if it wouldnâÂÂt be better to allocate the exact amount of memory requested, I explain the arguments in favor of the power-of-two approach.
However, adding a .shrink_to_fit() method gives the client code the ability to decide that wasted memory is a bigger concern for that application than heap fragmentation. If the application doesnâÂÂt call that, they get the default behavior.
edited Jul 13 at 0:58
answered Jul 12 at 18:21
Davislor
70128
70128
add a comment |Â
add a comment |Â
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f198359%2fmodern-vector-implementation%23new-answer', 'question_page');
);
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
4
Is there a reason for not simply using
std::vector?â Edward
Jul 12 at 12:19
6
If this is meant to be reinventing-the-wheel, you should add that tag (and consider using matching names such as
push_backandcbegin).â Toby Speight
Jul 12 at 13:58
2
inlineas a keyword for functions defined in class is useless because these functions getinlineautomatic.â Sandro4912
Jul 12 at 16:15
1
I don't think the code as posted was actually ready for a review. Please test your code before submitting it here.
â hoffmale
Jul 12 at 17:35
1
Why do you think that copy-and-swap idiom might reduce the speed?
â Toby Speight
Jul 12 at 18:15