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_back
andcbegin
).
â Toby Speight
Jul 12 at 13:58
2
inline
as a keyword for functions defined in class is useless because these functions getinline
automatic.
â 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_back
andcbegin
).
â Toby Speight
Jul 12 at 13:58
2
inline
as a keyword for functions defined in class is useless because these functions getinline
automatic.
â 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_back
andcbegin
).
â Toby Speight
Jul 12 at 13:58
2
inline
as a keyword for functions defined in class is useless because these functions getinline
automatic.
â 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_capacity
objects of typeT
in there.This might hurt performance a lot if
T
s default constructor is not trivial, and even then it still isn't necessary.Even worse, in
pushBack
a newT
object 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 ifT
s destructor is not trivial (e.g. it would leak memory).Consider using
std::aligned_storage
for 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_ptr
helps preventing those leaks.clear
leaks memory (no call todelete
).Some member functions (see below) set
m_capacity
,Log
and/orm_size
to unexpected values, which in turn can cause wrong allocations and/or out of bounds memory accesses in other places.reserve
accesses out of bounds memory ifcapac < m_size
due tonewbuff[i]
being out of bounds.For any given
m_capacity
, the first call topushBack
wherem_size >= m_capacity
doesn't allocate bigger space (Log
gets increased after the call toreserve
inreserve(1 << Log++)
).This means
Log
is out of sync fromm_capacity
,buff[m_size++]
is out of bounds andm_size > m_capacity
after the call.The constructors taking a
size
parameter initializem_capacity
beforeLog
(due to member definition order inside the class), but refer to the uninitialized value ofLog
for doing so. This setsm_capacity
to a wrong value, and thus allocates a wrong amount of memory forbuff
(ifsize > 0
).
pushBack
has a potential use-after-free error: Ifval
refers to an element inside thisVector
andVector
needs to reallocate,val
will dangle when the new object gets copy constructed.The copy assignment operator deletes
buff
without 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
buff
is 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
,cbegin
andcend
make this container unusable for standard algorithms.begin
andend
should provide aconst
overload, so they can be called on aconst Vector<T>&
.Similarly,
cbegin
andcend
should be declaredconst
.Also,
push_front
,pop_front
,emplace
,emplace_back
,emplace_front
,remove
,insert
and all the reverse iterator variants are missing.
Naming
- The style of names for member variables is inconsistent:
m_size
/m_capacity
,buff
andLog
. 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_back
methods to constructT
objects 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 provideconst
overloads forfront
,back
andoperator
.Vector<T>::swap
should likely bepublic
.Vector<T>
isn't copyable unlessT
itself is. Thus, the copy constructor and copy assignment operator should bedelete
d 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 theVector
to 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)
?buff
is already of typeT*
.inline
actually doesn't do anything in this implementation.clear()
doesn't call destructors for objects betweenm_size
andm_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::copy
is used for copying, sometimes a rawfor
loop. This is inconsistent.In some of those cases, they could be replaced by a range-based
std::move
(ifT
is 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 useLog
at all?)Try to
std::move
objects if possible. This can be asserted with template meta programming.You might consider using
realloc
for reallocations ifT
is 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_capacity
wrong, 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_capacity
objects of typeT
in there.This might hurt performance a lot if
T
s default constructor is not trivial, and even then it still isn't necessary.Even worse, in
pushBack
a newT
object 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 ifT
s destructor is not trivial (e.g. it would leak memory).Consider using
std::aligned_storage
for 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_ptr
helps preventing those leaks.clear
leaks memory (no call todelete
).Some member functions (see below) set
m_capacity
,Log
and/orm_size
to unexpected values, which in turn can cause wrong allocations and/or out of bounds memory accesses in other places.reserve
accesses out of bounds memory ifcapac < m_size
due tonewbuff[i]
being out of bounds.For any given
m_capacity
, the first call topushBack
wherem_size >= m_capacity
doesn't allocate bigger space (Log
gets increased after the call toreserve
inreserve(1 << Log++)
).This means
Log
is out of sync fromm_capacity
,buff[m_size++]
is out of bounds andm_size > m_capacity
after the call.The constructors taking a
size
parameter initializem_capacity
beforeLog
(due to member definition order inside the class), but refer to the uninitialized value ofLog
for doing so. This setsm_capacity
to a wrong value, and thus allocates a wrong amount of memory forbuff
(ifsize > 0
).
pushBack
has a potential use-after-free error: Ifval
refers to an element inside thisVector
andVector
needs to reallocate,val
will dangle when the new object gets copy constructed.The copy assignment operator deletes
buff
without 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
buff
is 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
,cbegin
andcend
make this container unusable for standard algorithms.begin
andend
should provide aconst
overload, so they can be called on aconst Vector<T>&
.Similarly,
cbegin
andcend
should be declaredconst
.Also,
push_front
,pop_front
,emplace
,emplace_back
,emplace_front
,remove
,insert
and all the reverse iterator variants are missing.
Naming
- The style of names for member variables is inconsistent:
m_size
/m_capacity
,buff
andLog
. 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_back
methods to constructT
objects 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 provideconst
overloads forfront
,back
andoperator
.Vector<T>::swap
should likely bepublic
.Vector<T>
isn't copyable unlessT
itself is. Thus, the copy constructor and copy assignment operator should bedelete
d 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 theVector
to 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)
?buff
is already of typeT*
.inline
actually doesn't do anything in this implementation.clear()
doesn't call destructors for objects betweenm_size
andm_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::copy
is used for copying, sometimes a rawfor
loop. This is inconsistent.In some of those cases, they could be replaced by a range-based
std::move
(ifT
is 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 useLog
at all?)Try to
std::move
objects if possible. This can be asserted with template meta programming.You might consider using
realloc
for reallocations ifT
is 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_capacity
objects of typeT
in there.This might hurt performance a lot if
T
s default constructor is not trivial, and even then it still isn't necessary.Even worse, in
pushBack
a newT
object 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 ifT
s destructor is not trivial (e.g. it would leak memory).Consider using
std::aligned_storage
for 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_ptr
helps preventing those leaks.clear
leaks memory (no call todelete
).Some member functions (see below) set
m_capacity
,Log
and/orm_size
to unexpected values, which in turn can cause wrong allocations and/or out of bounds memory accesses in other places.reserve
accesses out of bounds memory ifcapac < m_size
due tonewbuff[i]
being out of bounds.For any given
m_capacity
, the first call topushBack
wherem_size >= m_capacity
doesn't allocate bigger space (Log
gets increased after the call toreserve
inreserve(1 << Log++)
).This means
Log
is out of sync fromm_capacity
,buff[m_size++]
is out of bounds andm_size > m_capacity
after the call.The constructors taking a
size
parameter initializem_capacity
beforeLog
(due to member definition order inside the class), but refer to the uninitialized value ofLog
for doing so. This setsm_capacity
to a wrong value, and thus allocates a wrong amount of memory forbuff
(ifsize > 0
).
pushBack
has a potential use-after-free error: Ifval
refers to an element inside thisVector
andVector
needs to reallocate,val
will dangle when the new object gets copy constructed.The copy assignment operator deletes
buff
without 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
buff
is 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
,cbegin
andcend
make this container unusable for standard algorithms.begin
andend
should provide aconst
overload, so they can be called on aconst Vector<T>&
.Similarly,
cbegin
andcend
should be declaredconst
.Also,
push_front
,pop_front
,emplace
,emplace_back
,emplace_front
,remove
,insert
and all the reverse iterator variants are missing.
Naming
- The style of names for member variables is inconsistent:
m_size
/m_capacity
,buff
andLog
. 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_back
methods to constructT
objects 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 provideconst
overloads forfront
,back
andoperator
.Vector<T>::swap
should likely bepublic
.Vector<T>
isn't copyable unlessT
itself is. Thus, the copy constructor and copy assignment operator should bedelete
d 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 theVector
to 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)
?buff
is already of typeT*
.inline
actually doesn't do anything in this implementation.clear()
doesn't call destructors for objects betweenm_size
andm_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::copy
is used for copying, sometimes a rawfor
loop. This is inconsistent.In some of those cases, they could be replaced by a range-based
std::move
(ifT
is 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 useLog
at all?)Try to
std::move
objects if possible. This can be asserted with template meta programming.You might consider using
realloc
for reallocations ifT
is 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_capacity
objects of typeT
in there.This might hurt performance a lot if
T
s default constructor is not trivial, and even then it still isn't necessary.Even worse, in
pushBack
a newT
object 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 ifT
s destructor is not trivial (e.g. it would leak memory).Consider using
std::aligned_storage
for 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_ptr
helps preventing those leaks.clear
leaks memory (no call todelete
).Some member functions (see below) set
m_capacity
,Log
and/orm_size
to unexpected values, which in turn can cause wrong allocations and/or out of bounds memory accesses in other places.reserve
accesses out of bounds memory ifcapac < m_size
due tonewbuff[i]
being out of bounds.For any given
m_capacity
, the first call topushBack
wherem_size >= m_capacity
doesn't allocate bigger space (Log
gets increased after the call toreserve
inreserve(1 << Log++)
).This means
Log
is out of sync fromm_capacity
,buff[m_size++]
is out of bounds andm_size > m_capacity
after the call.The constructors taking a
size
parameter initializem_capacity
beforeLog
(due to member definition order inside the class), but refer to the uninitialized value ofLog
for doing so. This setsm_capacity
to a wrong value, and thus allocates a wrong amount of memory forbuff
(ifsize > 0
).
pushBack
has a potential use-after-free error: Ifval
refers to an element inside thisVector
andVector
needs to reallocate,val
will dangle when the new object gets copy constructed.The copy assignment operator deletes
buff
without 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
buff
is 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
,cbegin
andcend
make this container unusable for standard algorithms.begin
andend
should provide aconst
overload, so they can be called on aconst Vector<T>&
.Similarly,
cbegin
andcend
should be declaredconst
.Also,
push_front
,pop_front
,emplace
,emplace_back
,emplace_front
,remove
,insert
and all the reverse iterator variants are missing.
Naming
- The style of names for member variables is inconsistent:
m_size
/m_capacity
,buff
andLog
. 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_back
methods to constructT
objects 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 provideconst
overloads forfront
,back
andoperator
.Vector<T>::swap
should likely bepublic
.Vector<T>
isn't copyable unlessT
itself is. Thus, the copy constructor and copy assignment operator should bedelete
d 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 theVector
to 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)
?buff
is already of typeT*
.inline
actually doesn't do anything in this implementation.clear()
doesn't call destructors for objects betweenm_size
andm_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::copy
is used for copying, sometimes a rawfor
loop. This is inconsistent.In some of those cases, they could be replaced by a range-based
std::move
(ifT
is 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 useLog
at all?)Try to
std::move
objects if possible. This can be asserted with template meta programming.You might consider using
realloc
for reallocations ifT
is 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_capacity
objects of typeT
in there.This might hurt performance a lot if
T
s default constructor is not trivial, and even then it still isn't necessary.Even worse, in
pushBack
a newT
object 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 ifT
s destructor is not trivial (e.g. it would leak memory).Consider using
std::aligned_storage
for 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_ptr
helps preventing those leaks.clear
leaks memory (no call todelete
).Some member functions (see below) set
m_capacity
,Log
and/orm_size
to unexpected values, which in turn can cause wrong allocations and/or out of bounds memory accesses in other places.reserve
accesses out of bounds memory ifcapac < m_size
due tonewbuff[i]
being out of bounds.For any given
m_capacity
, the first call topushBack
wherem_size >= m_capacity
doesn't allocate bigger space (Log
gets increased after the call toreserve
inreserve(1 << Log++)
).This means
Log
is out of sync fromm_capacity
,buff[m_size++]
is out of bounds andm_size > m_capacity
after the call.The constructors taking a
size
parameter initializem_capacity
beforeLog
(due to member definition order inside the class), but refer to the uninitialized value ofLog
for doing so. This setsm_capacity
to a wrong value, and thus allocates a wrong amount of memory forbuff
(ifsize > 0
).
pushBack
has a potential use-after-free error: Ifval
refers to an element inside thisVector
andVector
needs to reallocate,val
will dangle when the new object gets copy constructed.The copy assignment operator deletes
buff
without 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
buff
is 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
,cbegin
andcend
make this container unusable for standard algorithms.begin
andend
should provide aconst
overload, so they can be called on aconst Vector<T>&
.Similarly,
cbegin
andcend
should be declaredconst
.Also,
push_front
,pop_front
,emplace
,emplace_back
,emplace_front
,remove
,insert
and all the reverse iterator variants are missing.
Naming
- The style of names for member variables is inconsistent:
m_size
/m_capacity
,buff
andLog
. 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_back
methods to constructT
objects 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 provideconst
overloads forfront
,back
andoperator
.Vector<T>::swap
should likely bepublic
.Vector<T>
isn't copyable unlessT
itself is. Thus, the copy constructor and copy assignment operator should bedelete
d 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 theVector
to 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)
?buff
is already of typeT*
.inline
actually doesn't do anything in this implementation.clear()
doesn't call destructors for objects betweenm_size
andm_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::copy
is used for copying, sometimes a rawfor
loop. This is inconsistent.In some of those cases, they could be replaced by a range-based
std::move
(ifT
is 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 useLog
at all?)Try to
std::move
objects if possible. This can be asserted with template meta programming.You might consider using
realloc
for reallocations ifT
is 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_capacity
wrong, 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_capacity
wrong, 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_capacity
wrong, 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_capacity
wrong, 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_back
andcbegin
).â Toby Speight
Jul 12 at 13:58
2
inline
as a keyword for functions defined in class is useless because these functions getinline
automatic.â 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