A dynamically-resizable vector with fixed capacity and embedded storage (revision -1)
Document number: none.
Date: none.
Author: Gonzalo Brito Gadeschi.
This paper proposes a dynamically-resizable vector
with fixed capacity and
contiguous embedded storage. That is, the elements of the vector are stored within
the vector object itself. This container is a modernized version of
boost::container::static_vector<T, Capacity>
that takes
advantage of C++17.
Its API is almost a 1:1 map of std::vector<T, A>
's API. It is a contiguous sequence
random-access container (with contiguous storage), non-amortized O(1)
insertion and removal of
elements at the end, and worst case O(size())
insertion and removal otherwise. Like
std::vector
, the elements are initialized on insertion and destroyed on
removal. It models ContiguousContainer
and its iterators model
the ContiguousIterator
concept.
This container is useful when:
- memory allocation is not possible, e.g., embedded environments without a free store, where only a stack and the static memory segment are available,
- memory allocation imposes an unacceptable performance penalty, e.g., with respect to latency,
- allocation of objects with complex lifetimes in the static-memory segment is required,
- non-default constructible objects must be stored such that
std::array
is not an option, - a dynamic resizable array is required within
constexpr
functions, - full control over the storage location of the vector elements is required.
In this section Frequently Asked Questions are answered, an overview of existing implementations is given, and the rationale behind the proposed design is provided.
Yes, in practice we can, but neither in a portable way, nor in a way that results
in a zero-cost abstraction, mainly due to the following limitations in the Allocator
interface.
-
The
Allocator::allocate(N)
member function either succeeds, returning a pointer to the storage forN
elements, or it fails. The current interface allows returning storage for more elements than requested, but it doesn't provideAllocators
a way to communicate this situation to the container, so the containers cannot make use of it. -
The
Allocator
interface does not provide a way to "extend" the storage in place -
The growth mechanism of
std::vector
is implementation defined. There is currently no way for anAllocator
with embedded storage to know how much memory a vector will try to allocate for a maximum given number of elements at compile time. This information is required for allocating a memory buffer large enough to satisfy the growth policy ofstd::vector
in its worst case. Even if this becomes achievable in a portable way, a stateful allocator with embedded storage would still need to allocate memory forgrowth_factor() * (Capacity - 1)
vector elements to accomodatestd::vector
's growth policy, which is far from optimal (and thus, zero cost). -
An
Allocator
with embedded storage forCapacity * sizeof(T)
elements makes storing data members for thedata
pointer and thecapacity
unnecessary instd::vector
implementation. In the currentAllocator
interface there is no mechanism to communicatestd::vector
that storing these data members is unnecessary. -
The
Allocator
interface does not specify whether containers should propagatenoexcept
ness offAllocator
member functions into their interfaces. AnAllocator
with embedded storage forCapacity
elements never throws on allocating memory, significantly improving the exception safety guarantees of multiplestd::vector
operations.
Improving the Allocator
interface to solve issue 1. is enough to make an implementation
based on std::vector
with an user-defined Allocator
be portable (this should probably be
pursuded in a different proposal) since the std::vector
constructors could
Allocator::allocate(0)
on construction and directly get memory forCapacity
elements.
However, in order to make this a zero-cost abstraction one would need
to solve issues 4 and 5 as well. Whether solving these issues is worth pursuing or not is
still an open problem. Solving issue 4 to avoid dupplicated copies of the data and capacity
members of vector could significantly complicate the Allocator
interface.
The author of this proposal does not think that it will be possible to solve issues 4 and 5 in
the near future. Still, standard library implementors are encouraged to reuse their std::vector
implementations when implementing embedded_vector
if they are able to do so. Even if that leads
to a solution to all the issues above, an embedded_vector
type is still an useful type to have
in the standard library independently of how it is defined (e.g. as a stand alone type, or as a type
alias of a std::vector
with a specific allocator).
Yes, we can, but no, it does not result in a zero-cost abstraction.
The paper
PR0274: Clump – A Vector-like Contiguous Sequence Container with Embedded Storage
proposes a new type, small_vector<T, N, Allocator>
, which is essentially a
std::vector<T, Allocator>
that performs a Small Vector Optimization for up to
N
elements, and then, depending on the Allocator
, might fall-back to heap allocations,
or do something else (like throw
, assert
, terminate
, introduce undefined behavior...).
This small vector type is part of Boost, LLVM,
EASTL, and Folly. Most of these libraries special case small_vector
for the case in which only embedded storage is desired. This result in a type with
slightly different but noticeable semantics (in the spirit of vector<bool>
). The
only library that offers it as a completely different type is Boost.Container.
The main difference between small_vector
and embedded_vector
is:
small_vector
uses embedded storage up-toN
elements, and then falls back toAllocator
, whileembedded_vector
only provides embedded storage.
As a consequence, for the use cases of embedded_vector
:
-
small_vector
cannot provide the same exception safety guarantees thanembedded_vector
because it sometimes allocates memory, whichembedded_vector
never does. -
small_vector
cannot provide the same reliability thanembedded_vector
, because the algorithmic complexity ofsmall_vector
operations like move construction/assignment depends on whether the elements are allocated on the heap (swap pointer and size) or embedded within the vector object itself (must always copy the elements). -
small_vector
cannot be as efficient asembedded_vector
. It must discriminate between the different storage location of its elements (embedded within the vector object or owned by the allocator). A part of its methods must, at run-time, branch to a different code-path depending on the active storage scheme.
The only way to fix small_vector
would be to special case it for
Allocator::max_size() == 0
(which isn't constexpr), and to provide an API that
has different complexity and exception-safety guarantees than small_vector
's
with Allocator::max_size() > 0
.
That is, to make small_vector
competitive in those situations in which
embedded_vector
is required is to special case small_vector
for a particular
allocator type and in that case provide an embedded_vector
implementation (with
slightly different semantics).
The types embedded_vector
and small_vector
have different algorithmic
complexity and exception-safety guarantees. They solve different problems and
should be different types.
The author has not tried but it might be possible to reuse this proposal to implement
embedded_vector
on top of it by defining a new Storage
type. Note however, that
the semantics of contiguous_container
highly depend on the concepts modeled by
the Storage
type. It is not even guaranteed that contiguous_container
models
DefaultConstructible
(it only does so conditionally, if Storage
does). So while
an implementation might be able to reuse contiguous_container
to implement
embedded_vector
, its interface, or that of its storage, would still need to be
specified.
There are at least 3 widely used implementations of embedded_vector
.
This proposal is strongly inspired by Boost.Container, which offers
boost::container::static_vector<T, Capacity>
(1.59),
and, as a different type, also offers boost::container::small_vector<T, N, Allocator>
as well.
The other two libraries that implement embedded_vector
are Folly and
EASTL. Both of these libraries implement it as a special case of
small_vector
(which in both of these libraries has 4 template parameters).
EASTL small_vector
is called fixed_vector<T, N, hasAllocator, OverflowAllocator>
and uses a boolean template parameter to
indicate if the only storage mode available is embedded storage. The
design documents of EASTL seem to predate this special casing
since they actually argue against it:
- special casing complicates the implementation of
small_vector
,- (without proof) the resulting code size increase would be larger than the "4 bytes" that can be saved in storage per vector for the special case.
The current implementation does, however, special case small_vector
for embedded
storage. No rationale for this decision is given in the design documentation.
Folly implements small_vector<T, N, NoHeap, size_type>
, where the tag type
NoHeap
is used to switch off heap-fall back. Folly allows customizing the
size_type
of small_vector
. No rationale for this design decision is available
in the documentation. A different size_type
can potentially be used to reduce
embedded_vector
s memory requirements.
The current design follows that of boost::container::static_vector<T, Capacity>
closely.
It introduces a new type std::experimental::embedded_vector<T, Capacity>
in the
<experimental/embedded_vector>
header. It is a pure library extension to the C++
standard library.
embedded_vector
is a dynamically-resizable contiguous random-access sequence container withO(1)
insert/erase at the end, andO(size())
insert/erase otherwise. Its elements are stored within the vector object itself. It modelsContiguousContainer
and its iterators model theContiguousIterator
concept.
A prototype implementation of this proposal is provided for standardization
purposes: http://github.com/gnzlbg/embedded_vector
.
The main drawback of introducing a new type is, as the
design document of the EASTL points out, increased code size.
Since this container type is opt-in, only those users that need it will pay
this cost. Common techniques to reduce code size where explored in the
prototype implementation (e.g. implementing Capacity
/value_type
agnostic
functionality in a base class) without success, but implementations are
encouraged to consider code-size as a quality of implementation issue.
This container requires that T
models Destructible
. If T
's destructor
throws the behavior is undefined.
Specializations of embedded_vector<T, Capacity>
model the ContiguousContainer
concept.
The elements of the vector are properly aligned to an alignof(T)
memory address.
The embedded_vector<T, Capacity>::size_type
is the smallest unsigned integer type
that can represent Capacity
.
If the container is not empty, the member function data()
returns a pointer such that
[data(), data() + size())
is a valid range and data() == addressof(front()) == addressof(*begin())
.
Otherwise, the result of data()
is unspecified.
Note: embedded_vector<T, Capacity>
cannot be an aggregate since it provides
user-defined constructors.
It is required that is_empty<embedded_vector<T, 0>>::value == true
and
that swap(embedded_vector<T, 0>&, embedded_vector<T, 0>&)
is noexcept(true)
.
The move semantics of embedded_vector<T, Capacity>
are equal to those of
std::array<T, Size>
. That is, after
embedded_vector a(10);
embedded_vector b(std::move(a));
the elements of a
have been moved element-wise into b
, the elements of a
are left in an initialized but unspecified state (have been moved from state),
the size of a
is not altered, and a.size() == b.size()
.
Note that this behavior differs from std::vector<T, Allocator>
, in particular
for the similar case in which std::propagate_on_container_move_assignment<Allocator>{}
is false
. In this situation the state of std::vector
is initialized but unspecified,
which prevents users from portably relying on size() == 0
or size() == N
, and raises
questions like "Should users call clear
after moving from a std::vector
?" (whose
answer is yes, in particular if propagate_on_container_move_assignment<Allocator>
is false
).
The whole API of embedded_vector<T, Capacity>
is constexpr
if is_trivial<T>
is true.
Implementations can achieve this by using a C array for embedded_vector
's storage
without altering the guarantees of embedded_vector
's methods in an observable way.
For example, embedded_vector(N)
constructor guarantees "exactly N
calls to
T
's default constructor". Strictly speaking, embedded_vector
's constructor
for trivial types will construct a C array of Capacity
length. However, because
is_trivial<T>
is true, the number of constructor or destructor calls is not
observable.
This introduces an additional implementation cost which the author believes is
worth it because similarly to std::array
, embedded_vector
s of trivial types
are incredibly common.
Note: this type of constexpr
support does not require any core language changes.
This design could, however, be both simplified and extended if 1) placement new
,
2) explicit destructor calls, and 3) reinterpret_cast
, would be constexpr
. This
paper does not propose any of these changes.
The class embedded_vector<T, Capacity>
can be explicitly instantiated for
all combinations of T
and Capacity
if T
satisfied the container preconditions
(i.e. T
models Destructible<T>
).
The only operations that can actually fail within embedded_vector<T, Capacity>
are:
-
T
special member functions and swap can only fail due to throwing constructors/assignment/destructors/swap ofT
. -
Mutating operations exceeding the capacity (
push_back
,insert
,emplace
,pop_back
whenempty()
,embedded_vector(T, size)
,embedded_vector(begin, end)
...). -
Out-of-bounds unchecked access: 2.1
front/back/pop_back
when empty, operator[] (unchecked random-access). 2.2at
(checked random-access) which can throwout_of_range
exception.
Three points influence the design of embedded_vector
with respect to its exception-safety guarantees:
- Making it a zero-cost abstraction.
- Making it safe to use.
- Making it easy to learn and use.
The best way to make embedded_vector
easy to learn is to make it as similar to std::vector
as possible. However,std::vector
allocates memory using an Allocator
, whose allocation
functions can throw, e.g., a std::bad_alloc
exception, e.g., on Out Of Memory.
However, embedded_vector
never allocates memory since its Capacity
is fixed at compile-time.
The main question then becomes, what should embedded_vector
do when its Capacity
is exceeded?
Two main choices were identified:
- Make it throw an exception.
- Make not exceeding the
Capacity
of anembedded_vector
a precondition on its mutating method (and thus exceeding it undefined-behavior).
While throwing an exception makes the interface more similar to that of std::vector
and safer to use, it does introduces a performance cost since it means that all the mutating methods must check for this condition. It also raises the question: which exception? It cannot be std::bad_alloc
, because nothing is being allocated.
It should probably be either std::out_of_bounds
or std::logic_error
, but if exceeding the capacity is a logic error, why don't we make it a precondition instead?
Making exceeding the capacity a precondition has some advantages:
-
It llows implementations to trivially provide a run-time diagnostic on debug builds by, e.g., means of an assertion.
-
It allows the methods to be conditionally marked
noexcept(true)
whenT
isstd::is_nothrow_default_constructible/copy_assignable>...
-
It makes
embedded_vector
a zero-cost abstraction by allowing the user to avoid unnecessary checks (e.g. hoisting checks out of a loop).
And this advantages come at the expense of safety. It is possible to have both by making the methods checked by default, but offering unchecked_xxx
alternatives that omit the checks which increases the API surface.
Given this design space, this proposal opts for making not exceeding the Capacity
of an embedded_vector
a precondition. It still allows some safety by allowing implementations to make the operations checked in the debug builds of their standard libraries, while providing very strong exception safety guarantees (and conditional noexcept(true)
), which makes embedded_vector
a true zero-cost abstraction.
The final question to be answered is if we should mark the mutating methods to be conditionally noexcept(true)
or not when it is safe to do so. The current proposal does so since it is easier to remove noexcept(...)
than to add it, and since this should allow the compiler to generate better code, which is relevant for some fields in which embedded_vector
is very useful, like in embedded systems programming.
If T
models Destructible
(that is, if T
destructor never throws),
embedded_vector<T, Capacity>
provides at least the basic-exception guarantee.
If T
does not model Destructible
, the behavior of embedded_vector
is undefined.
Implementations are encouraged to rely on T
modeling Destructible
even
if T
's destructor is noexcept(false)
.
If T
's special member functions and/or swap are noexcept(true)
, so are the respective
special member functions and/or swap operations of embedded_vector
which then provide the
strong-exception guarantee. They provide the basic-exception guarantee otherwise.
The capacity of embedded_vector
(a fixed-capacity vector) is statically known at
compile-time, that is, exceeding it is a logic error.
As a consequence, inserting elements beyond the Capacity
of an embedded_vector
results
in undefined behavior. While providing a run-time diagnostic in debug builds (e.g. via an
assertion
) is encouraged, this is a Quality of Implementation issue.
The algorithms that perform insertions are the constructors embedded_vector(T, size)
and
embedded_vector(begin, end)
, and the member functions push_back
, emplace_back
, insert
,
and resize
.
These algorithms provide strong-exception safety guarantee, and if T
's special member functions or
swap
can throw are noexcept(false)
, and noexcept(true)
otherwise.
Out-of-bounds unchecked access (front>back>pop_back
when empty, operator[]
) is undefined behavior
and a run-time diagnostic is encouraged but left as a Quality of Implementation issue.
These functions provide the strong-exception safety guarantee and are noexcept(true)
.
Checked access via at
provides the strong-exception safety guarantee and it throws the std::out_of_range
exception on out-of-bounds. It is noexcept(false)
.
The iterators of embedded_vector<T, Capacity>
model the ContiguousIterator
concept.
The iterator invalidation rules are different than those for std::vector
,
since:
- moving an
embedded_vector
invalidates all iterators, - swapping two
embedded_vector
s invalidates all iterators, and - inserting elements into an
embedded_vector
never invalidates iterators.
The following functions can potentially invalidate the iterators of embedded_vector
s:
resize(n)
, resize(n, v)
, pop_back
, erase
, and swap
.
The following functions from the "possible future extensions" can potentially
invalidate the iterators of embedded_vector
s: resize_default_initialized(n)
,
resize_unchecked(n)
, resize_unchecked(n, v)
, and
resize_default_initialized_unchecked(n)
.
Following names have been considered:
-
embedded_vector<T, Capacity>
: since the elements are "embedded" within the vector object itself. Sadly, the nameembedded
is overloaded, e.g., embedded systems, and while in this domain this container is very useful, it is not the only domain in which it is useful. -
fixed_capacity_vector
: a vector with fixed capacity, long name, but clearly indicates what this is. -
static_vector
(Boost.Container): due to "static" / compile-time allocation of the elements. The termstatic
is, however, overloaded in C++ (e.g.static
memory?). -
inline_vector
: the elements are stored "inline" within the vector object itself. The terminline
is, however, already overloaded in C++ (e.g.inline
functions => ODR, inlining,inline
variables). -
stack_vector
: to denote that the elements can be stored on the stack, which is confusing since the elements can be on the stack, the heap, or the static memory segment. It also has a resemblance withstd::stack
.
embedded_vector<T, Capacity>
is a pure library extension to the C++ standard library and can be implemented by any C++11 compiler in a separate header <embedded_vector>
.
A source of pain when using embedded vectors on API is that the vector Capacity
is part of its type. This is a problem worth solving, can be solved independently
of this proposal, and in a backwards compatible way with it.
To the author's best knowledge, the best way to solve this problem would be to
define an any_vector_view<T>
and any_vector<T>
types with reference and value
semantics respectively that use concept-based run-time polymorphism to erase
the type of the vector. These types would work with any vector-like type and provide
infinite extensibility. Among the types they could work with are std::vector
/ boost::vector
with different allocators, and embedded_vector
s and small_vector
s of different capacities
(and allocators for small_vector
).
A down-side of such an any_vector_view/any_vector
type is that its efficient
implementation would use virtual functions internally for type-erasure. Devirtualization
in non-trivial programs (multiple TUs) is still not a solved problem (not even
with the recent advances in LTO in modern compilers).
An any_embedded_vector_view<T>
that provides reference
semantics for embedded_vector
s can be implemented using the same
techniques as array_view<T>
without introducing virtual dispatch. This would
solve the pain points of using embedded_vector<T, Capacity>
on APIs.
It is possible to implement and propose those types in a future proposal, but doing so is clearly out of this proposal's scope.
The size-modifying operations of the embedded_vector
that do not require a value
also have the following analogous counterparts that perform default
initialization instead of value initialization:
struct default_initialized_t {};
inline constexpr default_initialized_t default_initialized{};
template <typename Value, std::size_t Capacity>
struct embedded_vector {
// ...
constexpr embedded_vector(default_initialized_t, size_type n);
constexpr void resize(default_initialized_t, size_type sz);
constexpr void resize_unchecked(default_initialized_t, size_type sz);
};
In the current proposal exceeding the capacity on the mutating operations is considered a logic-error and results in undefined behavior, which allows implementations to cheaply provide an assertion in debug builds without introducing checks in release builds. If a future revision of this paper changes this to an alternative solution that has an associated cost for checking the invariant, it might be worth it to consider adding support to unchecked mutating operations like resize_unchecked
,push_back_unchecked
, assign_unchecked
, emplace
, and insert
.
Consider:
using vec_t = embedded_vector<std::size_t, N>;
vec_t v0(2); // two-elements: 0, 0
vec_t v1{2}; // one-element: 2
vec_t v2(2, 1); // two-elements: 1, 1
vec_t v3{2, 1}; // two-elements: 2, 1
A way to avoid this problem introduced by initializer list and braced initialization, present in the interface of embedded_vector
and std::vector
, would be to use a tagged-constructor of the form embedded_vector(with_size_t, std::size_t N, T const& t = T())
to indicate that constructing a vector with N
elements is inteded. For std::vector
,
a similar constructor using a with_capacity_t
and maybe combinations thereof might make sense. This proposal
does not propose any of these, but this is a problem that should definetely be solved in STL2, and if it solved,
it should be solved for embedded_vector
as well.
This enhancement is a pure header-only addition to the C++ standard library as the <experimental/embedded_vector>
header.
template<typename T, std::size_t C /* Capacity */>
struct embedded_vector {
// types:
typedef value_type& reference;
typedef value_type const& const_reference;
typedef implementation-defined iterator;
typedef implementation-defined const_iterator;
typedef /*smallest unsigned integer type that is able to represent Capacity */ size_type;
typedef ptrdiff_t difference_type;
typedef T value_type;
typedef T* pointer;
typedef T const* const_pointer;
typedef reverse_iterator<iterator> reverse_iterator;
typedef reverse_iterator<const_iterator> const_reverse_iterator;
// construct/copy/move/destroy:
constexpr embedded_vector() noexcept;
constexpr explicit embedded_vector(size_type n);
constexpr embedded_vector(size_type n, const value_type& value);
template<class InputIterator>
constexpr embedded_vector(InputIterator first, InputIterator last);
constexpr embedded_vector(embedded_vector const& other)
noexcept(is_nothrow_copy_constructible<value_type>{});
constexpr embedded_vector(embedded_vector && other)
noexcept(is_nothrow_move_constructible<value_type>{});
constexpr embedded_vector(initializer_list<value_type> il);
/* constexpr ~embedded_vector(); */ // implicitly generated
constexpr embedded_vector& operator=(embedded_vector const& other)
noexcept(is_nothrow_copy_assignable<value_type>{});
constexpr embedded_vector& operator=(embedded_vector && other);
noexcept(is_nothrow_move_assignable<value_type>{});
template<class InputIterator>
constexpr void assign(InputIterator first, InputIterator last);
constexpr void assign(size_type n, const value_type& u);
constexpr void assign(initializer_list<value_type> il);
// iterators:
constexpr iterator begin() noexcept;
constexpr const_iterator begin() const noexcept;
constexpr iterator end() noexcept;
constexpr const_iterator end() const noexcept;
constexpr reverse_iterator rbegin() noexcept;
constexpr const_reverse_iterator rbegin() const noexcept;
constexpr reverse_iterator rend() noexcept;
constexpr const_reverse_iterator rend() const noexcept;
constexpr const_iterator cbegin() noexcept;
constexpr const_iterator cend() const noexcept;
constexpr const_reverse_iterator crbegin() noexcept;
constexpr const_reverse_iterator crend() const noexcept;
// size/capacity:
constexpr size_type size() const noexcept;
static constexpr size_type capacity() noexcept;
static constexpr size_type max_size() noexcept;
constexpr void resize(size_type sz);
constexpr void resize(size_type sz, const value_type& c)
constexpr bool empty() const noexcept;
void reserve(size_type n) = delete;
void shrink_to_fit() = delete;
// element access:
constexpr reference operator[](size_type n) noexcept;
constexpr const_reference operator[](size_type n) const noexcept;
constexpr const_reference at(size_type n) const;
constexpr reference at(size_type n);
constexpr reference front() noexcept;
constexpr const_reference front() const noexcept;
constexpr reference back() noexcept;
constexpr const_reference back() const noexcept;
// data access:
constexpr T* data() noexcept;
constexpr const T* data() const noexcept;
// modifiers:
template<class... Args>
constexpr void emplace_back(Args&&... args);
constexpr void push_back(const value_type& x);
constexpr void push_back(value_type&& x);
constexpr void pop_back();
template<class... Args>
constexpr iterator emplace(const_iterator position, Args&&...args)
constexpr iterator insert(const_iterator position, const value_type& x);
constexpr iterator insert(const_iterator position, value_type&& x);
constexpr iterator insert(const_iterator position, size_type n, const value_type& x);
template<class InputIterator>
constexpr iterator insert(const_iterator position, InputIterator first, InputIterator last);
constexpr iterator insert(const_iterator position, initializer_list<value_type> il);
constexpr iterator erase(const_iterator position)
noexcept(is_nothrow_destructible<value_type>{} and is_nothrow_swappable<value_type>{});
constexpr iterator erase(const_iterator first, const_iterator last)
noexcept(is_nothrow_destructible<value_type>{} and is_nothrow_swappable<value_type>{});
constexpr void clear() noexcept(is_nothrow_destructible<value_type>{});
constexpr void swap(embedded_vector&)
noexcept(noexcept(swap(declval<value_type&>(), declval<value_type&>()))));
friend constexpr bool operator==(const embedded_vector& a, const embedded_vector& b);
friend constexpr bool operator!=(const embedded_vector& a, const embedded_vector& b);
friend constexpr bool operator<(const embedded_vector& a, const embedded_vector& b);
friend constexpr bool operator<=(const embedded_vector& a, const embedded_vector& b);
friend constexpr bool operator>(const embedded_vector& a, const embedded_vector& b);
friend constexpr bool operator>=(const embedded_vector& a, const embedded_vector& b);
};
template <typename T, std::size_t Capacity>
constexpr void swap(embedded_vector<T, Capacity>&, embedded_vector<T, Capacity>&)
noexcept(is_nothrow_swappable<T>{});
constexpr embedded_vector() noexcept;
Constructs an empty
embedded_vector
.
Requirements: none.
Enabled: always.
Complexity:
- time: O(1),
- space: O(1).
Exception safety: never throws.
Constexpr: always.
Iterator invalidation: none.
Effects: none.
Post-condition:
size() == 0
.
constexpr explicit embedded_vector(size_type n);
Constructs an
embedded_vector
containingn
value-initialized elements.
Requirements:
value_type
shall beDefaultInsertable
into*this
.Enabled: if requirements are met.
Complexity:
- time: exactly
n
calls tovalue_type
's default constructor,- space: O(1).
Exception safety:
- strong guarantee: all constructed elements shall be destroyed on failure,
- re-throws if
value_type
's default constructor throws.Constexpr: if
is_trivial<value_type>
.Iterator invalidation: none.
Effects: exactly
n
calls tovalue_type
s default constructor.Pre-condition:
n <= Capacity
.Post-condition:
size() == n
.
constexpr embedded_vector(size_type n, const value_type& value);
Constructs an
embedded_vector
containingn
copies ofvalue
.
Requirements:
value_type
shall beCopyInsertable
into*this
.Enabled: if requirements are met.
Complexity:
- time: exactly
n
calls tovalue_type
's copy constructor,- space: O(1).
Exception safety:
- strong guarantee: all constructed elements shall be destroyed on failure,
- re-throws if
value_type
's copy constructor throws,Constexpr: if
is_trivial<value_type>
.Iterator invalidation: none.
Effects: exactly
n
calls tovalue_type
s copy constructor.Pre-condition:
n <= Capacity
.Post-condition:
size() == n
.
template<class InputIterator>
constexpr embedded_vector(InputIterator first, InputIterator last);
Constructs an
embedded_vector
containing a copy of the elements in the range[first, last)
.
Requirements:
value_type
shall be either:
CopyInsertable
into*this
if the reference type ofInputIterator
is an lvalue reference, or
MoveInsertable
into*this
if the reference type ofInputIterator
is an rvalue reference.Enabled: if requirements are met.
Complexity:
- time: exactly
last - first
calls tovalue_type
's copy or move constructor,- space: O(1).
Exception safety:
- strong guarantee: all constructed elements shall be destroyed on failure,
- re-throws if
value_type
's copy or move constructors throws,Constexpr: if
is_trivial<value_type>
.Iterator invalidation: none.
Effects: exactly
last - first
calls tovalue_type
s copy or move constructor.Pre-condition:
last - first <= Capacity
.Post-condition:
size() == last - first
.
constexpr embedded_vector(embedded_vector const& other);
noexcept(is_nothrow_copy_constructible<value_type>{});
Constructs a
embedded_vector
whose elements are copied fromother
.
Requirements:
value_type
shall beCopyInsertable
into*this
.Enabled: if requirements are met.
Complexity:
- time: exactly
other.size()
calls tovalue_type
's copy constructor,- space: O(1).
Exception safety:
- strong guarantee: all constructed elements shall be destroyed on failure,
- re-throws if
value_type
's copy constructor throws.Constexpr: if
is_trivial<value_type>
.Iterator invalidation: none.
Effects: exactly \p
other.size()
calls tovalue_type
s copy constructor.Pre-condition: none.
Post-condition:
size() == other.size()
.
constexpr embedded_vector(embedded_vector&& other)
noexcept(is_nothrow_move_constructible<value_type>{});
Constructs an
embedded_vector
whose elements are moved fromother
.
Requirements:
value_type
shall beMoveInsertable
into*this
.Enabled: if requirements are met.
Complexity:
- time: exactly
other.size()
calls tovalue_type
's move constructor,- space: O(1).
Exception safety:
- strong guarantee if
std::nothrow_move_assignable<T>
is true, basic guarantee otherwise: all moved elements shall be destroyed on failure.- re-throws if
value_type
's move constructor throws.Constexpr: if
is_trivial<value_type>
.Iterator invalidation: none.
Effects: exactly
other.size()
calls tovalue_type
s move constructor.Pre-condition: none.
Post-condition:
size() == other.size()
.Invariant:
other.size()
does not change.
/// Equivalent to `embedded_vector(il.begin(), il.end())`.
constexpr embedded_vector(initializer_list<value_type> il);
noexcept(is_nothrow_copy_constructible<value_type>{});
Move assignment operations invalidate iterators.
constexpr embedded_vector& operator=(embedded_vector const& other)
noexcept(is_nothrow_copy_assignable<value_type>{});
constexpr embedded_vector& operator=(embedded_vector && other);
noexcept(is_nothrow_move_assignable<value_type>{});
template<std::size_t M, enable_if_t<(C != M)>>
constexpr embedded_vector& operator=(embedded_vector<value_type, M>const& other)
noexcept(is_nothrow_copy_assignable<value_type>{} and C >= M);
template<std::size_t M, enable_if_t<(C != M)>>
constexpr embedded_vector& operator=(embedded_vector<value_type, M>&& other);
noexcept(is_nothrow_move_assignable<value_type>{} and C >= M);
template<class InputIterator>
constexpr void assign(InputIterator first, InputIterator last);
constexpr void assign(size_type n, const value_type& u);
constexpr void assign(initializer_list<value_type> il);
The destructor should be implicitly generated and it should be constexpr
if is_trivial<value_type>
.
/* constexpr ~embedded_vector(); */ // implicitly generated
For all iterator functions:
constexpr iterator begin() noexcept;
constexpr const_iterator begin() const noexcept;
constexpr iterator end() noexcept;
constexpr const_iterator end() const noexcept;
constexpr reverse_iterator rbegin() noexcept;
constexpr const_reverse_iterator rbegin() const noexcept;
constexpr reverse_iterator rend() noexcept;
constexpr const_reverse_iterator rend() const noexcept;
constexpr const_iterator cbegin() noexcept;
constexpr const_iterator cend() const noexcept;
constexpr const_reverse_iterator crbegin() noexcept;
constexpr const_reverse_iterator crend() const noexcept;
the following holds:
- Requirements: none.
- Enabled: always.
- Complexity: constant time and space.
- Exception safety: never throw.
- Constexpr: always.
- Effects: none.
The iterator
and const_iterator
types are implementation defined and model
the ContiguousIterator
concept.
There are also some guarantees between the results of data
and the iterator
functions that are explained in the section "Element / data access" below.
constexpr size_type size() const noexcept;
Returns: the number of elements that the vector currently holds.
Requirements: none.
Enabled: always.
Complexity: constant time and space.
Exception safety: never throws.
Constexpr: always.
Effects: none.
static constexpr size_type capacity() noexcept;
Returns: the total number of elements that the vector can hold.
Requirements: none.
Enabled: always.
Complexity: constant time and space.
Exception safety: never throws.
Constexpr: always.
Effects: none.
Note:
- if
capacity() == 0
, thensizeof(embedded_vector) == 0
,- if
sizeof(T) == 0 and capacity() > 0
, thensizeof(embedded_vector) == sizeof(unsigned char)
.
static constexpr size_type max_size() noexcept;
Returns: the total number of elements that the vector can hold.
Requirements: none.
Enabled: always.
Complexity: constant time and space.
Exception safety: never throws.
Constexpr: always.
Effects: none.
constexpr bool empty() const noexcept;
Returns:
true
if the total number of elements is zero,false
otherwise.Requirements: none.
Enabled: always.
Complexity: constant time and space.
Exception safety: never throws.
Constexpr: always.
Effects: none.
For the checked resize functions:
constexpr void resize(size_type new_size);
constexpr void resize(size_type new_size, const value_type& c);
the following holds:
- Requirements:
T
modelsDefaultInsertable
/CopyInsertable
. - Enabled: if requirements satisfied.
- Complexity: O(size()) time, O(1) space.
- Exception safety:
- basic guarantee: all constructed elements shall be destroyed on failure,
- rethrows if
value_type
's default or copy constructors throws, - throws
bad_alloc
ifnew_size > capacity()
.
- Constexpr: if
is_trivial<value_type>
. - Effects:
- if
new_size > size
exactlynew_size - size
elements default>copy constructed. - if
new_size < size
:- exactly
size - new_size
elements destroyed. - all iterators pointing to elements with
position > new_size
are invalidated.
- exactly
- if
For the unchecked element access functions:
constexpr reference operator[](size_type n) noexcept;
constexpr const_reference operator[](size_type n) const noexcept;
constexpr reference front() noexcept;
constexpr const_reference front() const noexcept;
constexpr reference back() noexcept;
constexpr const_reference back() const noexcept;
the following holds:
- Requirements: none.
- Enabled: always.
- Complexity: O(1) in time and space.
- Exception safety: never throws.
- Constexpr: if
is_trivial<value_type>
. - Effects: none.
- Pre-conditions:
size() > n
foroperator[]
,size() > 0
forfront
andback
.
For the checked element access functions:
constexpr const_reference at(size_type n) const;
constexpr reference at(size_type n);
the following holds:
- Requirements: none.
- Enabled: always.
- Complexity: O(1) in time and space.
- Exception safety:
- throws
out_of_range
ifn >= size()
.
- throws
- Constexpr: if
is_trivial<value_type>
. - Effects: none.
- Pre-conditions: none.
For the data access:
constexpr T* data() noexcept;
constexpr const T* data() const noexcept;
the following holds:
- Requirements: none.
- Enabled: always.
- Complexity: O(1) in time and space.
- Exception safety: never throws.
- Constexpr: if
is_trivial<value_type>
. - Effects: none.
- Pre-conditions: none.
- Returns: if the container is empty the return value is unspecified. If the container
is not empty,
[data(), data() + size())
is a valid range, anddata() == addressof(front())
.
For the modifiers:
template<class... Args>
constexpr void emplace_back(Args&&... args);
Construct a new element at the end of the vector in place using
args...
.
Requirements:
Constructible<value_type, Args...>
.Enabled: if requirements are met.
Complexity:
- time: O(1), exactly one call to
T
's constructor,- space: O(1).
Exception safety:
- strong guarantee: no side-effects if
value_type
's constructor throws.- re-throws if
value_type
's constructor throws.Constexpr: if
is_trivial<value_type>
.Iterator invalidation: none.
Effects: exactly one call to
T
's constructor, thesize()
of the vector is incremented by one.Pre-condition:
size() < Capacity
.Post-condition:
size() == size_before + 1
.
constexpr void push_back(const value_type& x);
Copy construct an element at the end of the vector from
x
.
Requirements:
CopyConstructible<value_type>
.Enabled: if requirements are met.
Complexity:
- time: O(1), exactly one call to
T
's copy constructor,- space: O(1).
Exception safety:
- strong guarantee: no side-effects if
value_type
's copy constructor throws.- re-throws if
value_type
's constructor throws.Constexpr: if
is_trivial<value_type>
.Iterator invalidation: none.
Effects: exactly one call to
T
's copy constructor, thesize()
of the vector is incremented by one.Pre-condition:
size() < Capacity
.Post-condition:
size() == size_before + 1
.
constexpr void push_back(value_type&& x);
Move construct an element at the end of the vector from
x
.
Requirements:
MoveConstructible<value_type>
.Enabled: if requirements are met.
Complexity:
- time: O(1), exactly one call to
T
's move constructor,- space: O(1).
Exception safety:
- strong guarantee: no side-effects if
value_type
's move constructor throws.- re-throws if
value_type
's constructor throws.Constexpr: if
is_trivial<value_type>
.Iterator invalidation: none.
Effects: exactly one call to
T
's move constructor, thesize()
of the vector is incremented by one.Pre-condition:
size() < Capacity
.Post-condition:
size() == size_before + 1
.
constexpr void pop_back();
Removes the last element from the vector.
Requirements: none.
Enabled: always.
Complexity:
- time: O(1), exactly one call to
T
's destructor,- space: O(1).
Exception safety:
- strong guarantee (note:
embedded_vector
requiresDestructible<T>
).Constexpr: if
is_trivial<value_type>
.Iterator invalidation: none.
Effects: exactly one call to
T
's destructor, thesize()
of the vector is decremented by one.Pre-condition:
size() > 0
.Post-condition:
size() == size_before - 1
.
constexpr iterator insert(const_iterator position, const value_type& x);
Stable inserts
x
atposition
within the vector (preserving the relative order of the elements in the vector).
Requirements:
CopyConstructible<value_type>
.Enabled: if requirements are met.
Complexity:
- time: O(size() + 1), exactly
end() - position
swaps, one call toT
's copy constructor,- space: O(1).
Exception safety:
- strong guarantee if
std::is_nothrow_swappable<value_type>
: no observable side-effects (note: even ifT
s copy constructor can throw).Constexpr: if
is_trivial<value_type>
.Iterator invalidation: all iterators pointing to elements after
position
are invalidated.Effects: exactly
end() - position
swaps, one call toT
's copy constructor, thesize()
of the vector is incremented by one.Pre-condition:
size() + 1 <= Capacity
,position
is in range[begin(), end())
.Post-condition:
size() == size_before + 1
.Invariant: the relative order of the elements before and after \p position is preserved.
constexpr iterator insert(const_iterator position, value_type&& x);
Stable inserts
x
atposition
(preserving the relative order of the elements in the vector).
Requirements:
MoveConstructible<value_type>
.Enabled: if requirements are met.
Complexity:
- time: O(size() + 1), exactly
end() - position
swaps, one call toT
's move constructor,- space: O(1).
Exception safety:
- strong guarantee if
std::is_nothrow_swappable<value_type>
: no observable side-effects (note: even ifT
s move constructor can throw).Constexpr: if
is_trivial<value_type>
.Iterator invalidation: all iterators pointing to elements after
position
are invalidated.Effects: exactly
end() - position
swaps, one call toT
's move constructor, thesize()
of the vector is incremented by one.Pre-condition:
size() + 1 <= Capacity
,position
is in range[begin(), end())
.Post-condition:
size() == size_before - 1
.Invariant: the relative order of the elements before and after
position
is preserved.
constexpr iterator insert(const_iterator position, size_type n, const value_type& x);
Stable inserts
n
copies ofx
atposition
(preserving the relative order of the elements in the vector).
Requirements:
CopyConstructible<value_type>
.Enabled: if requirements are met.
Complexity:
- time: O(size() + n), exactly
end() - position + n - 1
swaps,n
calls toT
's copy constructor,- space: O(1).
Exception safety:
- strong guarantee if
std::is_nothrow_swappable<value_type>
: no observable side-effects (note: even ifT
s copy constructor can throw).Constexpr: if
is_trivial<value_type>
.Iterator invalidation: all iterators pointing to elements after
position
are invalidated.Effects: exactly
end() - position + n - 1
swaps,n
calls toT
's copy constructor, thesize()
of the vector is incremented byn
.Pre-condition:
size() + n <= Capacity
,position
is in range[begin(), end())
.Post-condition:
size() == size_before + n
.Invariant: the relative order of the elements before and after
position
is preserved.
template <typename InputIterator>
constexpr iterator insert(const_iterator position, InputIterator first, InputIterator last);
Stable inserts the elements of the range
[first, last)
atposition
(preserving the relative order of the elements in the vector).
Requirements:
Constructible<value_type, iterator_traits<InputIt>::value_type>
,InputIterator<InputIt>
.Enabled: if requirements are met.
Complexity:
- time: O(size() + distance(first, last)), exactly
end() - position + distance(first, last) - 1
swaps,n
calls toT
's copy constructor (note: independently ofInputIt
's iterator category),- space: O(1).
Exception safety:
- strong guarantee if
std::is_nothrow_swappable<value_type>
: no observable side-effects (note: even ifT
s copy constructor can throw).Constexpr: if
is_trivial<value_type>
.Iterator invalidation: all iterators pointing to elements after
position
are invalidated.Effects: exactly
end() - position + distance(first, last) - 1
swaps,n
calls toT
's copy constructor, thesize()
of the vector is incremented byn
.Pre-condition:
size() + distance(first, last) <= Capacity
,position
is in range[begin(), end())
,[first, last)
is not a sub-range of[position, end())
.Post-condition:
size() == size_before + distance(first, last)
.Invariant: the relative order of the elements before and after
position
is preserved.
/// Equivalent to `insert(position, begin(il), end(il))`.
constexpr iterator insert(const_iterator position, initializer_list<value_type> il);
constexpr iterator erase(const_iterator position)
noexcept(is_nothrow_destructible<value_type>{} and is_nothrow_swappable<value_type>{});
Stable erases the element at
position
(preserving the relative order of the elements in the vector).
Requirements: none.
Enabled: always.
Complexity:
time: O(size()), exactly
end() - position - 1
swaps, 1 call toT
's destructor.space: O(1).
Exception safety:
strong guarantee if
std::is_nothrow_swappable<value_type>
: no observable side-effects.Constexpr: if
is_trivial<value_type>
.Iterator invalidation: all iterators pointing to elements after
position
are invalidated.Effects: exactly
end() - position - 1
swaps, 1 call toT
's destructor, thesize()
of the vector is decremented by 1.Pre-condition:
size() - 1 >= 0
,position
is in range[begin(), end())
.Post-condition:
size() == size_before - 1
,size() >= 0
.Invariant: the relative order of the elements before and after
position
is preserved.
constexpr iterator erase(const_iterator first, const_iterator last)
noexcept(is_nothrow_destructible<value_type>{} and is_nothrow_swappable<value_type>{});
Stable erases the elements in range
[first, last)
(preserving the relative order of the elements in the vector).
Requirements: none.
Enabled: always.
Complexity:
time: O(size()), exactly
end() - first - distance(first, last)
swaps,distance(first, last)
calls toT
's destructor.space: O(1).
Exception safety:
strong guarantee if
std::is_nothrow_swappable<value_type>
: no observable side-effects.Constexpr: if
is_trivial<value_type>
.Iterator invalidation: all iterators pointing to elements after
first
are invalidated.Effects: exactly
end() - first - distance(first, last)
swaps,distance(first, last)
calls toT
's destructor, thesize()
of the vector is decremented bydistance(first, last)
.Pre-condition:
size() - distance(first, last) >= 0
,[first, last)
is a sub-range of[begin(), end())
.Post-condition:
size() == size_before - distance(first, last)
,size() >= 0
.Invariant: the relative order of the elements before and after
position
remains unchanged.
/// Equivalent to `erase(begin(), end())`.
constexpr void clear() noexcept(is_nothrow_destructible<value_type>{});
constexpr void swap(embedded_vector& other)
noexcept(noexcept(swap(declval<value_type&>(), declval<value_type&>()))));
Swaps the elements of two vectors.
Requirements: none.
Enabled: always.
Complexity:
- time: O(max(size(), other.size())), exactly
max(size(), other.size())
swaps.- space: O(1).
Exception safety:
strong guarantee if
std::is_nothrow_swappable<value_type>
: no observable side-effects.Constexpr: if
is_trivial<value_type>
.Iterator invalidation: all iterators pointing to the elements of both vectors are invalidated.
Effects: exactly
max(size(), other.size())
swaps.Pre-condition: none.
Post-condition:
size() == other_size_before
,other.size() == size_before
.
The following operators are noexcept
if the operations required to compute them are all noexcept
:
constexpr bool operator==(const embedded_vector& a, const embedded_vector& b);
constexpr bool operator!=(const embedded_vector& a, const embedded_vector& b);
constexpr bool operator<(const embedded_vector& a, const embedded_vector& b);
constexpr bool operator<=(const embedded_vector& a, const embedded_vector& b);
constexpr bool operator>(const embedded_vector& a, const embedded_vector& b);
constexpr bool operator>=(const embedded_vector& a, const embedded_vector& b);
The following holds for the comparison operators:
- Requirements: only enabled if
value_type
supports the corresponding operations. - Enabled: if requirements are met.
- Complexity: for two vectors of sizes
N
andM
, the complexity isO(1)
ifN != M
, and the comparison operator ofvalue_type
is invoked at mostN
times otherwise. - Exception safety:
noexcept
if the comparison operator ofvalue_type
isnoexcept
, otherwise can only throw if the comparison operator can throw.
The following people have significantly contributed to the development of this proposal.
First, the authors of Boost.Container's boost::container::static_vector
(Adam Wulkiewicz,
Andrew Hundt, and Ion Gaztanaga), on which this proposal is based. Second, to Howard Hinnant for libc++
<algorithm>
and <vector>
headers, and in particular, for the <vector>
test
suite which was extremely useful while prototyping an implementation. Andrzej
Krzemieński provided an example that shows that using tags is better than
using static member functions for "special constructors" (like the default initialized
constructor). And finally, to Casey Carter for his very detailed invaluable feedback on lots of aspects of this proposal.
- Boost.Container::static_vector.
- Discussions in the Boost developers mailing list:
- Boost.Container::small_vector.
- Howard Hinnant's stack_alloc.
- EASTL fixed_vector and design.
- Folly small_vector.
- LLVM small_vector.