I'm trying to expand on the implementation of static_vector on the std::aligned_storage reference page, but would like to split it into two parts. First, an aligned_storage_array that supports perfect forwarding (so that I can emplace into it) and doesn't require a default constructor, and then the actual static_vector infrastructure that builds upon it. This will let me use the aligned_storage_array for some other static data structures I plan to make in the future.
aligned_storage_array.h
#pragma once
#include <array>
#include <memory>
#include <stdexcept>
namespace nonstd
{
template<class T, std::size_t N>
class aligned_storage_array
{
public:
aligned_storage_array() = default;
~aligned_storage_array() = default;
// Move and copy must be manually implemented per-element by the user
aligned_storage_array(aligned_storage_array&& rhs) = delete;
aligned_storage_array& operator=(aligned_storage_array&& rhs) = delete;
aligned_storage_array(const aligned_storage_array& rhs) = delete;
aligned_storage_array& operator=(const aligned_storage_array& rhs) = delete;
// Size
constexpr std::size_t size() const noexcept { return N; }
constexpr std::size_t max_size() const noexcept { return N; }
// Access
inline T& operator[](std::size_t pos)
{
return *std::launder(
reinterpret_cast<T*>(
std::addressof(m_data[pos])));
}
inline const T& operator[](std::size_t pos) const
{
return *std::launder(
reinterpret_cast<const T*>(
std::addressof(m_data[pos])));
}
inline T& at(std::size_t pos)
{
return *std::launder(
reinterpret_cast<T*>(
std::addressof(m_data.at(pos))));
}
inline const T& at(std::size_t pos) const
{
return *std::launder(
reinterpret_cast<const T*>(
std::addressof(m_data.at(pos))));
}
// Operations
template<typename ...Args>
inline T& emplace(size_t pos, Args&&... args)
{
return
*::new(std::addressof(m_data[pos]))
T(std::forward<Args>(args)...);
}
template<typename ...Args>
inline T& bounded_emplace(size_t pos, Args&&... args)
{
return
*::new(std::addressof(m_data.at(pos)))
T(std::forward<Args>(args)...);
}
inline void destroy(std::size_t pos)
{
std::destroy_at(
std::launder(
reinterpret_cast<const T*>(
std::addressof(m_data[pos]))));
}
inline void bounded_destroy(std::size_t pos)
{
std::destroy_at(
std::launder(
reinterpret_cast<const T*>(
std::addressof(m_data.at(pos)))));
}
private:
std::array<std::aligned_storage_t<sizeof(T), alignof(T)>, N> m_data;
};
}
static_vector.h
#pragma once
#include <array>
#include <stdexcept>
#include "aligned_storage_array.h"
namespace nonstd
{
template<class T, std::size_t N>
class static_vector
{
public:
using value_type = T;
using pointer = T*;
using const_pointer = const T*;
using reference = value_type&;
using const_reference = const value_type&;
using iterator = value_type*;
using const_iterator = const value_type*;
using size_type = std::size_t;
static_vector() = default;
~static_vector() { clear(); }
static_vector(const static_vector& rhs)
{
clear(); // Sets m_size to zero for safety
for (std::size_t pos = 0; pos < rhs.m_size; ++pos)
m_data[pos] = rhs.m_data[pos];
m_size = rhs.m_size;
}
static_vector& operator=(const static_vector& rhs)
{
if (this != std::addressof(rhs))
{
clear(); // Sets m_size to zero for safety
for (std::size_t pos = 0; pos < rhs.m_size; ++pos)
m_data[pos] = rhs.m_data[pos];
m_size = rhs.m_size;
}
return *this;
}
static_vector(static_vector&& rhs)
{
// Start by clearing sizes to avoid bad data
// access in the case of an exception
std::size_t count_self = m_size;
std::size_t count_rhs = rhs.m_size;
m_size = 0;
rhs.m_size = 0;
// Can't swap because the destination may be uninitialized
destroy_n(count_self);
for (std::size_t pos = 0; pos < count_rhs; ++pos)
m_data[pos] = std::move(rhs.m_data[pos]);
m_size = count_rhs;
}
static_vector& operator=(static_vector&& rhs)
{
// Start by clearing sizes to avoid bad data
// access in the case of an exception
std::size_t count_self = m_size;
std::size_t count_rhs = rhs.m_size;
m_size = 0;
rhs.m_size = 0;
// Can't swap because the destination may be uninitialized
destroy_n(count_self);
for (std::size_t pos = 0; pos < count_rhs; ++pos)
m_data[pos] = std::move(rhs.m_data[pos]);
m_size = count_rhs;
return *this;
}
// Size and capacity
constexpr std::size_t size() const { return m_size; }
constexpr std::size_t max_size() const { return N; }
constexpr bool empty() const { return m_size == 0; }
// Iterators
inline iterator begin() { return &m_data[0]; }
inline const_iterator begin() const { return &m_data[0]; }
inline iterator end() { return &m_data[m_size]; }
inline const_iterator end() const { return &m_data[m_size]; }
// Access
inline T& operator[](std::size_t pos)
{
return m_data[pos];
}
inline const T& operator[](std::size_t pos) const
{
return m_data[pos];
}
inline T& at(std::size_t pos)
{
if ((pos < 0) || (pos >= m_size))
throw std::out_of_range("static_vector subscript out of range");
return m_data[pos];
}
inline const T& at(std::size_t pos) const
{
if ((pos < 0) || (pos >= m_size))
throw std::out_of_range("static_vector subscript out of range");
return m_data[pos];
}
// Operations
template<typename ...Args>
inline T& emplace_back(Args&&... args)
{
T& result = m_data.bounded_emplace(m_size, args...);
++m_size;
return result;
}
inline void clear()
{
std::size_t count = m_size;
m_size = 0; // In case of exception
destroy_n(count);
}
private:
void destroy_n(std::size_t count)
{
for (std::size_t pos = 0; pos < count; ++pos)
m_data.destroy(pos);
}
aligned_storage_array<T, N> m_data;
std::size_t m_size = 0;
};
}
A full testing apparatus is available here (wandbox). Mostly I'd like some extra eyes to help determine:
- Is this actually safe for placement new with respect to alignment?
- Is the use of
std::laundercorrect? - Is the use of
reinterpret_castcorrect (or should it be twostatic_casts instead?) - Are there any hidden pitfalls I should watch out for here (aside from the maximum capacity)?
- Am I being sufficiently paranoid (
::new,std::address_of,std::destroy_at)? Any other safety features I can put in to handle risky operator overloads? - Is this approach to copy/move correct? I feel as though I need to be hands-on because the underlying array may have unknown uninitialized fields. Am I doing the right thing about exception safety? I decided I'd rather have the vector appear empty than have it appear full with malformed entries.
- What should I do, if anything, about an
std::swapon either data structure?
I'm told this is similar to something like boost::small_vector, but I want the aligned_storage_array generalized because I want to use it for a number of different static structures later. Also, I would like to learn more about alignment/placement new, forwarding, and launder.