Because resizing a vector is expensive; the std::vector
class uses exponential growth to minimize the number of times that the vector is resized: a technique we replicate in this version. But every now and then you still need to resize the internal buffer.
In the current version, resizing the vector requires allocating a new buffer and copying all the members into it. Basically we are using the copy and swap idiom to provide the strong exception guarantee (If an exception is thrown all resources are cleaned up and the object remains unchanged).
Vector Resize with Copy
void pushBackInternal(T const& value)
{
new (buffer + length) T(value);
++length;
}
void reserveCapacity(std::size_t newCapacity)
{
Vector<T> tmpBuffer(newCapacity);
std::for_each(buffer, buffer + length,
[&tmpBuffer](T const& v){tmpBuffer.pushBackInternal(v);}
);
tmpBuffer.swap(*this);
}
Resize With Move Construction
Thus resizing a Vector
can be a very expensive operation because of all the copying that can happen.
Using the move constructor rather than the copy constructor during a resize operation could potentially be much more efficient. But the move constructor mutates the original object and thus if there is a problem we need to undo the mutations to maintain the strong exception guarantee.
The first attempt at this is:
Vector Resize with Move With Exceptions
void moveBackInternal(T&& value)
{
new (buffer + length) T(std::move(value));
++length;
}
void reserveCapacity(std::size_t newCapacity)
{
Vector<T> tmpBuffer(newCapacity);
try
{
std::for_each(buffer, buffer + length,
[&tmpBuffer](T& v){tmpBuffer.moveBackInternal(std::move(v));}
);
}
catch(...)
{
for(int loop=0; loop < tmpBuffer.length; ++loop)
{
buffer[loop] = std::move(tmpBuffer[loop]);
}
throw;
}
tmpBuffer.swap(*this);
}
Resize With NoThrow Move Construction
As the above code shows; if the type T
can throw during its move constructor then you can't guarantee that the object gets returned to the original state (as moving the already moved elements back may cause another exception). So we cannot use the move constructor to resize the vector if the type T
can throw during move construction.
But not all types throw when being moved. In fact it is recommended that move constructors never throw. If we can guarantee that the move constructor does not throw then we can simplify the above code considerably and still provide the strong exception guarantee.
Vector Resize with Move
void reserveCapacity(std::size_t newCapacity)
{
Vector<T> tmpBuffer(newCapacity);
std::for_each(buffer, buffer + length,
[&tmpBuffer](T& v){tmpBuffer.moveBackInternal(std::move(v));}
);
tmpBuffer.swap(*this);
}
void moveBackInternal(T&& value)
{
new (buffer + length) T(std::move(value));
++length;
}
Resize Template Specialization
So now we have to write the code that decides at compile time which version we should use. The simplest way to do this is to use template specialization of a class using the standard class std::is_nothrow_move_constructible<T>
to help differentiate types that have a non-throwing move constructor. This is simple enough:
Template class Specialization
template<typename T, bool = std::is_nothrow_move_constructible<T>::value>
struct SimpleCopy
{
void operator()(Vector<T>& src, Vector<T>& dst) const;
};
template<typename T>
class Vector
{
public:
.....
private:
friend struct SimpleCopy<T, true>;
friend struct SimpleCopy<T, false>;
void reserveCapacity(std::size_t newCapacity)
{
Vector<T> tmpBuffer(newCapacity);
SimpleCopy<T> copier;
copier(*this, tmpBuffer);
tmpBuffer.swap(*this);
}
void pushBackInternal(T const& value)
{
new (buffer + length) T(value);
++length;
}
void moveBackInternal(T&& value)
{
new (buffer + length) T(std::move(value));
++length;
}
}
template<typename T>
struct SimpleCopy<T, false>
{
void operator()(Vector<T>& src, Vector<T>& dst) const
{
std::for_each(buffer, buffer + length,
[&dst](T const& v){dst.pushBackInternal(v);}
);
}
};
template<typename T>
struct SimpleCopy<T, true>
{
void operator()(Vector<T>& src, Vector<T>& dst) const
{
std::for_each(buffer, buffer + length,
[&dst](T& v){dst.moveBackInternal(std::move(v));}
);
}
};
Resize With NoThrow SFINAE
The above technique has a couple of issues.
The type SimpleClass
is publicly available and is a friend of Vector<T>
. This makes it susceptible to accidentally being used (even if not explicitly documented). Unfortunately it can't be included as a member class and also be specialized.
Additionally it looks awful!!
But we can also use SFINAE and method overloading.
SFINAE allows us to define several versions of a method with exactly the same arguments, as long as only one of them is valid at compile time. So in the example below we define two versions of the method SimpleCopy(Vector<T>& src, Vector<T>& dst)
but then use std::enable_if
to make sure only one version of the function is valid at compile time.
SFINAE method overload
template<typename T>
class Vector
{
public:
.....
private:
void reserveCapacity(std::size_t newCapacity)
{
Vector<T> tmpBuffer(newCapacity);
SimpleCopy<T>(*this, tmpBuffer);
tmpBuffer.swap(*this);
}
void pushBackInternal(T const& value)
{
new (buffer + length) T(value);
++length;
}
void moveBackInternal(T&& value)
{
new (buffer + length) T(std::move(value));
++length;
}
template<typename X>
typename std::enable_if<std::is_nothrow_move_constructible<X>::value == false>::type
simpleCopy(Vector<T>& src, Vector<T>& dst)
{
std::for_each(buffer, buffer + length,
[&dst](T const& v){dst.pushBackInternal(v);}
);
}
template<typename X>
typename std::enable_if<std::is_nothrow_move_constructible<X>::value == true>::type
simpleCopy()(Vector<T>& src, Vector<T>& dst)
{
std::for_each(buffer, buffer + length,
[&dst](T& v){dst.moveBackInternal(std::move(v));}
);
}
}
Final Version
Vector Final Version
template<typename T>
class Vector
{
std::size_t capacity;
std::size_t length;
T* buffer;
struct Deleter
{
void operator()(T* buffer) const
{
::operator delete(buffer);
}
};
public:
Vector(int capacity = 10)
: capacity(capacity)
, length(0)
, buffer(static_cast<T*>(::operator new(sizeof(T) * capacity)))
{}
~Vector()
{
std::unique_ptr<T, Deleter> deleter(buffer, Deleter());
for(int loop = 0; loop < length; ++loop)
{
buffer[length - 1 - loop].~T();
}
}
Vector(Vector const& copy)
: capacity(copy.length)
, length(0)
, buffer(static_cast<T*>(::operator new(sizeof(T) * capacity)))
{
try
{
for(int loop = 0; loop < copy.length; ++loop)
{
push_back(copy.buffer[loop]);
}
}
catch(...)
{
std::unique_ptr<T, Deleter> deleter(buffer, Deleter());
for(int loop = 0; loop < length; ++loop)
{
buffer[length - 1 - loop].~T();
}
throw;
}
}
Vector& operator=(Vector const& copy)
{
Vector<T> tmp(copy);
tmp.swap(*this);
return *this;
}
Vector(Vector&& move) noexcept
: capacity(0)
, length(0)
, buffer(nullptr)
{
move.swap(*this);
}
Vector& operator=(Vector&& move) noexcept
{
move.swap(*this);
return *this;
}
void swap(Vector& other) noexcept
{
using std::swap;
swap(capacity, other.capacity);
swap(length, other.length);
swap(buffer, other.buffer);
}
void push_back(T const& value)
{
resizeIfRequire();
pushBackInternal(value);
}
void pop_back()
{
--length;
buffer[length].~T();
}
void reserve(std::size_t capacityUpperBound)
{
if (capacityUpperBound > capacity)
{
reserveCapacity(capacityUpperBound);
}
}
private:
void resizeIfRequire()
{
if (length == capacity)
{
std::size_t newCapacity = std::max(2.0, capacity * 1.5);
reserveCapacity(newCapacity);
}
}
void reserveCapacity(std::size_t newCapacity)
{
Vector<T> tmpBuffer(newCapacity);
simpleCopy<T>(tmpBuffer);
tmpBuffer.swap(*this);
}
void pushBackInternal(T const& value)
{
new (buffer + length) T(value);
++length;
}
void moveBackInternal(T&& value)
{
new (buffer + length) T(std::move(value));
++length;
}
template<typename X>
typename std::enable_if<std::is_nothrow_move_constructible<X>::value == false>::type
simpleCopy(Vector<T>& dst)
{
std::for_each(buffer, buffer + length,
[&dst](T const& v){dst.pushBackInternal(v);}
);
}
template<typename X>
typename std::enable_if<std::is_nothrow_move_constructible<X>::value == true>::type
simpleCopy(Vector<T>& dst)
{
std::for_each(buffer, buffer + length,
[&dst](T& v){dst.moveBackInternal(std::move(v));}
);
}
};
Summary
This article has gone over the design of resizing the internal buffer. We have covered a couple of techniques on the way:
- Move Constructor Concepts
- Template Class Specialization
- SFINAE
- std::is_nothrow_move_constructible<>
- std::enable_if<>