viboes / embedded_vector

A dynamically-resizable vector with inline storage

Home Page:https://github.com/gnzlbg/embedded_vector

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Embedded vector

A dynamically-resizable vector with fixed capacity and embedded storage (revision -1)

Document number: none.

Date: none.

Author: Gonzalo Brito Gadeschi.

Introduction

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.

Motivation

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.

Design

In this section Frequently Asked Questions are answered, an overview of existing implementations is given, and the rationale behind the proposed design is provided.

FAQ

Can we reuse std::vector with a custom allocator?

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.

  1. The Allocator::allocate(N) member function either succeeds, returning a pointer to the storage for N elements, or it fails. The current interface allows returning storage for more elements than requested, but it doesn't provide Allocators a way to communicate this situation to the container, so the containers cannot make use of it.

  2. The Allocator interface does not provide a way to "extend" the storage in place

  3. The growth mechanism of std::vector is implementation defined. There is currently no way for an Allocator 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 of std::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 for growth_factor() * (Capacity - 1) vector elements to accomodate std::vector's growth policy, which is far from optimal (and thus, zero cost).

  4. An Allocator with embedded storage for Capacity * sizeof(T) elements makes storing data members for the data pointer and the capacity unnecessary in std::vector implementation. In the current Allocator interface there is no mechanism to communicate std::vector that storing these data members is unnecessary.

  5. The Allocator interface does not specify whether containers should propagate noexceptness off Allocator member functions into their interfaces. An Allocator with embedded storage for Capacity elements never throws on allocating memory, significantly improving the exception safety guarantees of multiple std::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::vectorconstructors 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).

Can we reuse small_vector?

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-to N elements, and then falls back to Allocator, while embedded_vector only provides embedded storage.

As a consequence, for the use cases of embedded_vector:

  • small_vector cannot provide the same exception safety guarantees than embedded_vector because it sometimes allocates memory, which embedded_vector never does.

  • small_vector cannot provide the same reliability than embedded_vector, because the algorithmic complexity of small_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 as embedded_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).

Should we special case small_vector for embedded-storage-only like EASTL and Folly do?

The types embedded_vector and small_vector have different algorithmic complexity and exception-safety guarantees. They solve different problems and should be different types.

Can we reuse P0494R0 - contiguous_container proposal?

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.

Existing practice

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_vectors memory requirements.

Proposed design and rationale

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 with O(1) insert/erase at the end, and O(size()) insert/erase otherwise. Its elements are stored within the vector object itself. It models ContiguousContainer and its iterators model the ContiguousIterator 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.

Preconditions

This container requires that T models Destructible. If T's destructor throws the behavior is undefined.

Storage/Memory Layout

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.

Zero-sized

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).

Move semantics

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).

Constexpr-support

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_vectors 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.

Explicit instantiatiability

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>).

Exception Safety

What could possibly go wrong?

The only operations that can actually fail within embedded_vector<T, Capacity> are:

  1. T special member functions and swap can only fail due to throwing constructors/assignment/destructors/swap of T.

  2. Mutating operations exceeding the capacity (push_back, insert, emplace, pop_back when empty(), embedded_vector(T, size), embedded_vector(begin, end)...).

  3. Out-of-bounds unchecked access: 2.1 front/back/pop_back when empty, operator[] (unchecked random-access). 2.2 at (checked random-access) which can throw out_of_range exception.

Rationale

Three points influence the design of embedded_vector with respect to its exception-safety guarantees:

  1. Making it a zero-cost abstraction.
  2. Making it safe to use.
  3. 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:

  1. Make it throw an exception.
  2. Make not exceeding the Capacity of an embedded_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) when T is std::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.

Precondition on T modelling Destructible

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).

Exception-safety guarantees of special member functions and swap

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.

Exception-safety guarantees of algorithms that perform insertions

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.

Exception-safety guarantees of unchecked access

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).

Exception-safety guarantees of checked access

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).

Iterators

The iterators of embedded_vector<T, Capacity> model the ContiguousIterator concept.

Iterator invalidation

The iterator invalidation rules are different than those for std::vector, since:

  • moving an embedded_vector invalidates all iterators,
  • swapping two embedded_vectors invalidates all iterators, and
  • inserting elements into an embedded_vector never invalidates iterators.

The following functions can potentially invalidate the iterators of embedded_vectors: 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_vectors: resize_default_initialized(n), resize_unchecked(n), resize_unchecked(n, v), and resize_default_initialized_unchecked(n).

Naming

Following names have been considered:

  • embedded_vector<T, Capacity>: since the elements are "embedded" within the vector object itself. Sadly, the name embedded 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 term static is, however, overloaded in C++ (e.g. static memory?).

  • inline_vector: the elements are stored "inline" within the vector object itself. The term inline 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 with std::stack.

Impact on the standard

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>.

Future extensions

Interoperability of embedded vectors with different capacities

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_vectors and small_vectors 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_vectors 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.

Default initialization

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);
};

Unchecked mutating operations

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.

with_size / with_capacity constructors

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.

Technical specification

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>{});

Construction

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 containing n value-initialized elements.

  • Requirements: value_type shall be DefaultInsertable into *this.

  • Enabled: if requirements are met.

  • Complexity:

    • time: exactly n calls to value_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 to value_types default constructor.

  • Pre-condition: n <= Capacity.

  • Post-condition: size() == n.

constexpr embedded_vector(size_type n, const value_type& value);

Constructs an embedded_vector containing n copies of value.

  • Requirements: value_type shall be CopyInsertable into *this.

  • Enabled: if requirements are met.

  • Complexity:

    • time: exactly n calls to value_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 to value_types 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 of InputIterator is an lvalue reference, or

  • MoveInsertable into *this if the reference type of InputIterator is an rvalue reference.

  • Enabled: if requirements are met.

  • Complexity:

    • time: exactly last - first calls to value_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 to value_types 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 from other.

  • Requirements: value_type shall be CopyInsertable into *this.

  • Enabled: if requirements are met.

  • Complexity:

    • time: exactly other.size() calls to value_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 to value_types 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 from other.

  • Requirements: value_type shall be MoveInsertable into *this.

  • Enabled: if requirements are met.

  • Complexity:

    • time: exactly other.size() calls to value_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 to value_types 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>{});

Assignment

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);

Destruction

The destructor should be implicitly generated and it should be constexpr if is_trivial<value_type>.

/* constexpr ~embedded_vector(); */ // implicitly generated

Iterators

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.

Size / capacity

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, then sizeof(embedded_vector) == 0,
    • if sizeof(T) == 0 and capacity() > 0, then sizeof(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 models DefaultInsertable/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 if new_size > capacity().
  • Constexpr: if is_trivial<value_type>.
  • Effects:
    • if new_size > size exactly new_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.

Element / data access

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 for operator[], size() > 0 for front and back.

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 if n >= size().
  • 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, and data() == addressof(front()).

Modifiers

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, the size() 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, the size() 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, the size() 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 requires Destructible<T>).
  • Constexpr: if is_trivial<value_type>.

  • Iterator invalidation: none.

  • Effects: exactly one call to T's destructor, the size() 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 at position 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 to T's copy constructor,
    • space: O(1).
  • Exception safety:

    • strong guarantee if std::is_nothrow_swappable<value_type>: no observable side-effects (note: even if Ts 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 to T's copy constructor, the size() 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 at position (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 to T's move constructor,
    • space: O(1).
  • Exception safety:

    • strong guarantee if std::is_nothrow_swappable<value_type>: no observable side-effects (note: even if Ts 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 to T's move constructor, the size() 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 of x at position (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 to T's copy constructor,
    • space: O(1).
  • Exception safety:

    • strong guarantee if std::is_nothrow_swappable<value_type>: no observable side-effects (note: even if Ts 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 to T's copy constructor, the size() of the vector is incremented by n.

  • 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) at position (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 to T's copy constructor (note: independently of InputIt's iterator category),
    • space: O(1).
  • Exception safety:

    • strong guarantee if std::is_nothrow_swappable<value_type>: no observable side-effects (note: even if Ts 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 to T's copy constructor, the size() of the vector is incremented by n.

  • 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 to T'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 to T's destructor, the size() 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 to T'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 to T's destructor, the size() of the vector is decremented by distance(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.

Comparison operators

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 and M, the complexity is O(1) if N != M, and the comparison operator of value_type is invoked at most N times otherwise.
  • Exception safety: noexcept if the comparison operator of value_type is noexcept, otherwise can only throw if the comparison operator can throw.

Acknowledgments

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.

References

About

A dynamically-resizable vector with inline storage

https://github.com/gnzlbg/embedded_vector


Languages

Language:C++ 43.9%Language:CMake 40.0%Language:Python 11.1%Language:HTML 5.0%