Git Product home page Git Product logo

static_vector's Introduction

static_vector

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

Document number: P0843r3.

Date: 2019-01-20.

Project: Programming Language C++, Library Working Group.

Audience: LEWG.

Reply-to: Gonzalo Brito Gadeschi <gonzalo.gadeschi at rwth-aachen dot de>.

Table of contents

Changelog

Revision 4

  • LEWG suggested that push_back should be UB when the capacity is exceeded
  • LEWG suggested that this should be a free-standing header

Revision 3

  • Include LWG design questions for LEWG.
  • Incorporates LWG feedback.

Revision 2

  • Replace the placeholder name fixed_capacity_vector with static_vector
  • Remove at checked element access member function.
  • Add changelog section.

Revision 1

  • Minor style changes and bugfixes.

Design Questions from LWG to LEWG

LWG asks LEWG to re-consider the following two design decisions:

  • In this document, exceeding the capacity in methods like static_vector::push_back is a pre-condition violation, that is, if the capacity is exceeded, the behavior is undefined. LWG suggested that exceeding the capacity in these methods should std::abort instead. The trade-offs in this space are discussed in Section 4.4 Exception safety of this proposal.

  • In this document, <static_vector> is a free-standing header, this is now clarified in Section 5. Technical Specification. LWG suggests that static_vector should be included in <vector> instead.

1. Introduction

This paper proposes a modernized version of boost::container::static_vector<T,Capacity> [1]. That is, a dynamically-resizable vector with compile-time fixed capacity and contiguous embedded storage in which the elements are stored within the vector object itself.

Its API closely resembles that of std::vector<T, A>. It is a contiguous container with O(1) insertion and removal of elements at the end (non-amortized) and worst case O(size()) insertion and removal otherwise. Like std::vector, the elements are initialized on insertion and destroyed on removal. For trivial value_types, the vector is fully usable inside constexpr functions.

2. Motivation and Scope

The static_vector 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,
  • std::array is not an option, e.g., if non-default constructible objects must be stored,
  • a dynamically-resizable array is required within constexpr functions,
  • the storage location of the static_vector elements is required to be within the static_vector object itself (e.g. to support memcopy for serialization purposes).

3. Existing practice

There are at least 3 widely used implementations of static_vector: Boost.Container [1], EASTL [2], and Folly [3]. The main difference between these is that Boost.Container implements static_vector as a standalone type with its own guarantees, while both EASTL and Folly implement it by adding an extra template parameter to their small_vector types.

A static_vector can also be poorly emulated by using a custom allocator, like for example Howard Hinnant's stack_alloc [4], on top of std::vector.

This proposal shares a similar purpose with P0494R0 [5] and P0597R0: std::constexpr_vector<T> [6]. The main difference is that this proposal closely follows boost::container::static_vector [1] and proposes to standardize existing practice. A prototype implementation of this proposal for standardization purposes is provided here: http://github.com/gnzlbg/fixed_capacity_vector.

4. Design Decisions

The most fundamental question that must be answered is:

Should static_vector be a standalone type or a special case of some other type?

The EASTL [2] and Folly [3] special case small_vector, e.g., using a 4th template parameter, to make it become a static_vector. The paper P0639R0: Changing attack vector of the constexpr_vector [7] proposes improving the Allocator concepts to allow static_vector, among others, to be implemented as a special case of std::vector with a custom allocator.

Both approaches run into the same fundamental issue: static_vector methods are identically-named to those of std::vector yet they have subtly different effects, exception-safety, iterator invalidation, and complexity guarantees.

This proposal follows boost::container::static_vector<T,Capacity> [1] closely and specifies the semantics that static_vector ought to have as a standalone type. As a library component this delivers immediate value.

I hope that having the concise semantics of this type specified will also be helpful for those that want to generalize the Allocator interface to allow implementing static_vector as a std::vector with a custom allocator.

4.1 Storage/Memory Layout

The container models ContiguousContainer. The elements of the static_vector are contiguously stored and properly aligned within the static_vector object itself. The exact location of the contiguous elements within the static_vector is not specified. If the Capacity is zero the container has zero size:

static_assert(is_empty_v<static_vector<T, 0>>); // for all T

This optimization is easily implementable, enables the EBO, and felt right.

4.2 Move semantics

The move semantics of static_vector<T, Capacity> are equal to those of std::array<T, Size>. That is, after

static_vector a(10);
static_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: this behavior differs from std::vector<T, Allocator>, in particular for the similar case in which std::allocator_traits<Allocator>::propagate_on_container_move_assignment is false. In this situation the state of std::vector is initialized but unspecified.

4.3 constexpr support

The API of static_vector<T, Capacity> is constexpr. If is_trivially_copyable_v<T> && is_default_constructible_v<T> is true, static_vectors can be seamlessly used from constexpr code. This allows using static_vector as a constexpr_vector to, e.g., implement other constexpr containers.

The implementation cost of this is small: the prototye implementation specializes the storage for trivial types to use a C array with value-initialized elements and a defaulted destructor.

This changes the algorithmic complexity of static_vector constructors for trivial-types from "Linear in N" to "Constant in Capacity. When the value-initialization takes place at run-time, this difference in behavior might be signficiant: static_vector<non_trivial_type, 38721943228473>(4) will only initialize 4 elements but static_vector<trivial_type, 38721943228473>(4) must value-initialize the 38721943228473 - 4 excess elements to be a valid constexpr constructor.

Very large static_vector's are not the target use case of this container class and will have, in general, worse performance than, e.g., std::vector (e.g. due to moves being O(N)).

Future improvements to constexpr (e.g. being able to properly use std::aligned_storage in constexpr contexts) allow improving the performance of static_vector in a backwards compatible way.

4.4 Exception Safety

The only operations that can actually fail within static_vector<value_type, Capacity> are:

  1. value_type's constructors/assignment/destructors/swap can potentially throw,

  2. Mutating operations exceeding the capacity (push_back, insert, emplace, static_vector(value_type, size), static_vector(begin, end)...).

  3. Out-of-bounds unchecked access:

    • 3.1 front/back/pop_back when empty, operator[] (unchecked random-access).

When value_type's operations are invoked, the exception safety guarantees of static_vector depend on whether these operations can throw. This is detected with noexcept.

Since its Capacity is fixed at compile-time, static_vector never dynamically allocates memory, the answer to the following question determines the exception safety for all other operations:

What should static_vector do when its Capacity is exceeded?

Three main answers were explored in the prototype implementation:

  1. Throw an exception.
  2. Abort the process.
  3. Make this a precondition violation.

Throwing an exception is appealing because it makes the interface slightly more similar to that of std::vector. However, which exception should be thrown? It cannot be std::bad_alloc, because nothing is being allocated. It could throw either std::out_of_bounds or std::logic_error but in any case the interface does not end up being equal to that of std::vector.

Aborting the process avoids the perils of undefined behavior but comes at the cost of enforcing a particular "error handling" mechanism in the implementation, which would not allow extending it to use, e.g., Contracts, in the future.

The alternative is to make not exceeding the capacity a precondition on the static_vector's methods. This approach allows implementations to provide good run-time diagnostics if they so desire, e.g., on debug builds by means of an assertion, and makes implementation that avoid run-time checks conforming as well. Since the mutating methods have a precondition, they have narrow contracts, and are not conditionally noexcept. This provides implementations that desire throwing an exception the freedom to do so, and it also provides the standard the freedom to improve these APIs by using contracts in the future.

This proposal previously chooses this path and makes exceeding the static_vector's capacity a precondition violation that results in undefined behavior. Throwing checked_xxx methods can be provided in a backwards compatible way.

4.5 Iterator invalidation

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

  • moving a static_vector invalidates all iterators,
  • swapping two static_vectors invalidates all iterators, and
  • inserting elements at the end of an static_vector never invalidates iterators.

The following functions can potentially invalidate the iterators of static_vectors: resize(n), resize(n, v), pop_back, erase, and swap.

4.6 Naming

The static_vector name was chosen after considering the following names via a poll in LEWG:

  • array_vector: a vector whose storage is backed up by a raw array.
  • bounded_vector: clearly indicates that the the size of the vector is bounded.
  • fixed_capacity_vector: clearly indicates that the capacity is fixed.
  • static_capacity_vector: clearly indicates that the capacity is fixed at compile time (static is overloaded).
  • 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?).
  • embedded_vector<T, Capacity>: since the elements are "embedded" within the fixed_capacity_vector object itself. Sadly, the name embedded is overloaded, e.g., embedded systems.
  • inline_vector: the elements are stored "inline" within the fixed_capacity_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. 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.
  • limited_vector
  • vector_n

The names static_vector and vector_n tied in the number of votes. Many users are already familiar with the most widely implementation of this container (boost::container::static_vector), which gives static_vector an edge over a completely new name.

4.7 Future extensions

The following extensions could be added in a backwards compatible way:

  • utilities for hiding the concrete type of vector-like containers (e.g. any_vector_ref<T>/any_vector<T>).

  • default-initialization of the vector elements (as opposed to value-initialization): e.g. by using a tagged constructor with a default_initialized_t tag.

  • tagged-constructor of the form static_vector(with_size_t, std::size_t N, T const& t = T()) to avoid the complexity introduced by initializer lists and braced initialization:

using vec_t = static_vector<std::size_t, Capacity>;
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

All these extensions are generally useful and not part of this proposal.

5. Technical specification


Note to editor: This enhancement is a pure header-only addition to the C++ standard library as the freestanding <static_vector> header. It belongs in the "Sequence containers" (\ref{sequences}) part of the "Containers library" (\ref{containers}) as "Class template static_vector".

Note to LWG: one of the primary use cases for this container is embedded/freestanding. An alternative to adding a new <static_vector> header would be to add static_vector to any of the freestanding headers. None of the current freestanding headers is a good semantic fit.


5. Class template static_vector

Changes to library.requirements.organization.headers table "C++ library headers", add a new header: <static_vector>.

Changes to library.requirements.organization.compliance table "C++ headers for freestanding implementations", add a new row:

[static_vector] Static vector <static_vector>

Changes to container.requirements.general.

The note of Table "Container Requirements" should be changed to contain static_vector as well:

Those entries marked “(Note A)” or “(Note B)” have linear complexity 
- for array
+ for array and static_vector
and have constant complexity for all other standard containers. 
[ Note: The algorithm equal() is defined in [algorithms]. — end note ]

Changes to sequence.reqmts.1:

The library provides four basic kinds of sequence containers: vector,
- forward_­list, list, and deque.
+ static_vector, forward_­list, list, and deque.

Changes to sequence.reqmts.2:

vector is the type of sequence container that should be used by default. 
+ static_vector should be used when the container has a fixed capacity known during translation.
array should be used when the container has a fixed size known during translation. 

5.1 Class template static_vector overview

    1. A static_vector is a contiguous container that supports constant time insert and erase operations at the end; insert and erase in the middle take linear time. Its capacity is part of its type and its elements are stored within the static_vector object itself, meaning that that if v is a static_vector<T, N> then it obeys the identity &v[n] == &v[0] + n for all 0 <= n <= v.size().
    1. A static_vector satisfies the container requirements (\ref{container.requirements}) with the exception of the swap member function, whose complexity is linear instead of constant. It satisfies the sequence container requirements, including the optional sequence container requirements (\ref{sequence.reqmts}), with the exception of the push_front, pop_front, and emplace_front member functions, which are not provided. It satisfies the reversible container (\ref{container.requirements}) and contiguous container (\ref{container.requirements.general}) requirements. Descriptions are provided here only for operations on static_vector that are not described in one of these tables or for operations where there is additional semantic information.
    1. Class static_vector relies on the implicitly-declared special member functions (\ref{class.default.ctor}, \ref{class.dtor}, and \ref{class.copy.ctor}) to conform to the container requirements table in \ref{container.requirements}. In addition to the requirements specified in the container requirements table, the move constructor and move assignment operator for array require that T be Cpp17MoveConstructible or Cpp17MoveAssignable, respectively.
namespace std {

template <typename T, size_t N>
class static_vector {
public:
// types:
using value_type = T;
using pointer = T*;
using const_pointer = const T*; 
using reference = value_type&;
using const_reference = const value_type&;
using size_type =  size_t;
using difference_type = ptrdiff_t;
using iterator = implementation-defined;  // see [container.requirements]
using const_iterator = implementation-defined; // see [container.requirements]
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;

// 5.2, copy/move construction:
constexpr static_vector() noexcept;
constexpr static_vector(const static_vector&);
constexpr static_vector(static_vector&&);
constexpr explicit static_vector(size_type n);
constexpr static_vector(size_type n, const value_type& value);
template <class InputIterator>
constexpr static_vector(InputIterator first, InputIterator last);
constexpr static_vector(initializer_list<value_type> il);

// 5.3, copy/move assignment:
constexpr static_vector& operator=(const static_vector& other)
  noexcept(is_nothrow_copy_assignable_v<value_type>);
constexpr static_vector& operator=(static_vector&& other);
  noexcept(is_nothrow_move_assignable_v<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);

// 5.4, destruction
~static_vector();

// 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()  const noexcept;
constexpr const_iterator         cend()    const noexcept;
constexpr const_reverse_iterator crbegin() const noexcept;
constexpr const_reverse_iterator crend()   const noexcept;

// 5.5, size/capacity:
constexpr bool empty() const noexcept;
constexpr size_type size() const noexcept;
static constexpr size_type max_size() noexcept;
static constexpr size_type capacity() noexcept;
constexpr void resize(size_type sz);
constexpr void resize(size_type sz, const value_type& c);

// 5.6, element and data access:
constexpr reference       operator[](size_type n); 
constexpr const_reference operator[](size_type n) const;
constexpr reference       front();
constexpr const_reference front() const;
constexpr reference       back();
constexpr const_reference back() const;
constexpr       T* data()       noexcept;
constexpr const T* data() const noexcept;

// 5.7, modifiers:
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);

template <class... Args>
  constexpr iterator emplace(const_iterator position, Args&&... args);
template <class... Args>
  constexpr reference emplace_back(Args&&... args);
constexpr void push_back(const value_type& x);
constexpr void push_back(value_type&& x);

constexpr void pop_back();
constexpr iterator erase(const_iterator position);
constexpr iterator erase(const_iterator first, const_iterator last);

constexpr void clear() noexcept;

constexpr void swap(static_vector& x)
  noexcept(is_nothrow_swappable_v<value_type> &&
           is_nothrow_move_constructible_v<value_type>);
};

template <typename T, size_t N>
constexpr bool operator==(const static_vector<T, N>& a, const static_vector<T, N>& b);
template <typename T, size_t N>
constexpr bool operator!=(const static_vector<T, N>& a, const static_vector<T, N>& b);
template <typename T, size_t N>
constexpr bool operator<(const static_vector<T, N>& a, const static_vector<T, N>& b);
template <typename T, size_t N>
constexpr bool operator<=(const static_vector<T, N>& a, const static_vector<T, N>& b);
template <typename T, size_t N>
constexpr bool operator>(const static_vector<T, N>& a, const static_vector<T, N>& b);
template <typename T, size_t N>
constexpr bool operator>=(const static_vector<T, N>& a, const static_vector<T, N>& b);

// 5.8, specialized algorithms:
template <typename T, size_t N>
constexpr void swap(static_vector<T, N>& x, static_vector<T, N>& y)
  noexcept(noexcept(x.swap(y)));
  
}  // namespace std

5.2 static_vector constructors


constexpr static_vector() noexcept;
  • Effects: Constructs an empty static_vector.

  • Ensures: empty().

  • Complexity: Constant.


constexpr static_vector(static_vector&& rv);
  • Effects: Constructs a static_vector by move-inserting the elements of rv.

  • Mandates: std::is_move_constructivle<value_type>.

  • Ensures: The static_vector is equal to the value that rv had before this construction.

  • Complexity: Linear in rv.size().


constexpr explicit static_vector(size_type n);
  • Effects: Constructs a static_vector with n default-inserted elements.

  • Mandates: std::is_default_constructible<value_type>.

  • Expects: n <= capacity().

  • Complexity: Linear in n.


constexpr static_vector(size_type n, const value_type& value);
  • Effects: Constructs a static_vector with n copies of value.

  • Mandates: std::is_copy_constructible<value_type>

  • Expects: n <= capacity().

  • Complexity: Linear in n.


template <class InputIterator>
constexpr static_vector(InputIterator first, InputIterator last);
  • Effects: Constructs a static_vector equal to the range [first, last)

  • Mandates: std::is_constructible<value_type, decltype(*first)>

  • Expects: distance(first, last) <= capacity()

  • Complexity: Linear in distance(first, last).

5.3 Destruction

~static_vector();

Effects: Destroys the static_vector and its elements.

Remarks: This destructor is trivial if the destructor of value_type is trivial.

5.4 Size and capacity

static constexpr size_type capacity() noexcept
static constexpr size_type max_size() noexcept
  • Returns: N.

constexpr void resize(size_type sz);
  • Effects: If sz < size(), erases the last size() - sz elements from the sequence. Otherwise, appends sz - size() default-constructed elements.

  • Mandates: std::is_default_constructible<value_type>.

  • Expects: sz <= capacity().

  • Complexity: Linear in sz.


constexpr void resize(size_type sz, const value_type& c);
  • Effects: If sz < size(), erases the last size() - sz elements from the sequence. Otherwise, appends sz - size() copies of c.

  • Mandates: std::is_copy_constructible<value_type>.

  • Expects: sz <= capacity().

  • Complexity: Linear in sz.

5.5 Element and data access

constexpr       T* data()       noexcept;
constexpr const T* data() const noexcept;
  • Returns: A pointer such that [data(), data() + size()) is a valid range. For a non-empty static_vector, data() == addressof(front()).

  • Complexity: Constant time.

5.6 Modifiers


Note to LWG: All modifiers have a pre-condition on not exceeding the capacity() when inserting elements. That is, exceeding the capacity() of the vector is undefined behavior. This supports some of the major use cases of this container (embedded, freestanding, etc.) and was required by the stakeholders during LEWG review. Currently, this provides maximum freedom to the implementation to choose an appropriate behavior: abort, assert, throw an exception (which exception? bad_alloc? logic_error? out_of_bounds? etc. ). In the future, this freedom allows us to specify these pre-conditions using contracts.

Note to LWG: Because all modifiers have preconditions, they all have narrow contracts and are not unconditionally noexcept.


constexpr iterator insert(const_iterator position, const value_type& x);
  • Effects: Inserts x at position and invalidates all references to elements after position.

  • Expects: size() < capacity().

  • Mandates: std::is_copy_constructible<value_type>.

  • Complexity: Linear in size().

  • Remarks: If an exception is thrown by value_type's copy constructor and is_nothrow_move_constructible_v<value_type> is true there are no effects. Otherwise, if an exception is thrown by value_type's copy constructor the effects are unspecified.


constexpr iterator insert(const_iterator position, size_type n, const value_type& x);
  • Effects: Inserts n copies of x at position and invalidates all references to elements after position.

  • Expects: n <= capacity() - size().

  • Mandates: std::is_copy_constructible<value_type>.

  • Complexity: Linear in size() and n.

  • Remarks: If an exception is thrown by value_type's copy constructor and is_nothrow_move_constructible_v<value_type> is true there are no effects. Otherwise, if an exception is thrown by value_type's copy constructor the effects are unspecified.


constexpr iterator insert(const_iterator position, value_type&& x);
  • Effects: Inserts x at position and invalidates all references to elements after position.

  • Expects: size() < capacity().

  • Mandates: std::is_move_constructible<value_type>.

  • Complexity: Linear in size().

  • Remarks: If an exception is thrown by value_type's move constructor the effects are unspecified.


template <typename InputIterator>
constexpr iterator insert(const_iterator position, InputIterator first, InputIterator last);
  • Effects: Inserts elements in range [first, last) at position and invalidates all references to elements after position.

  • Expects: distance(first, last) <= capacity() - size().

  • Mandates: std::is_constructible<value_type, decltype(*first)>.

  • Complexity: Linear in size() and distance(first, last).

  • Remarks: If an exception is thrown by value_type constructor from decltype(*first) and is_nothrow_move_constructible_v<value_type> is true there are no effects. Otherwise, if an exception is thrown by value_type's constructor from decltype(*first) the effects are unspecified.


constexpr iterator insert(const_iterator position, initializer_list<value_type> il);
  • Effects: Inserts elements of il at position and invalidates all references to elements after position.

  • Expects: il.size() <= capacity() - size().

  • Mandates: std::is_copy_constructible<value_type>.

  • Complexity: Linear in size() and il.size().

  • Remarks: If an exception is thrown by value_type's copy constructor and is_nothrow_move_constructible_v<value_type> is true there are no effects. Otherwise, if an exception is thrown by value_type's copy constructor the effects are unspecified.


template <class... Args>
constexpr iterator emplace(const_iterator position, Args&&... args);
  • Effects: Inserts an element constructed from args... at position and invalidates all references to elements after position.

  • Expects: size() < capacity().

  • Mandates: std::is_constructible<value_type, Args...>.

  • Complexity: Linear in size().

  • Remarks: If an exception is thrown by value_type's constructor from args... and is_nothrow_move_constructible_v<value_type> is true there are no effects. Otherwise, if an exception is thrown by value_type's constructor from args... the effects are unspecified.


template <class... Args>
constexpr reference emplace_back(Args&&... args);
  • Effects: Inserts an element constructed from args... at the end.

  • Expects: size() < capacity().

  • Mandates: std::is_constructible<value_type, Args...>.

  • Complexity: Constant.

  • Remarks: If an exception is thrown by value_type's constructor from args... there are no effects.


constexpr void push_back(const value_type& x);
  • Effects: Inserts a copy of x at the end.

  • Expects: size() < capacity().

  • Mandates: std::is_copy_constructible<value_type>.

  • Complexity: Constant.

  • Remarks: If an exception is thrown by value_type's copy constructor there are no effects.


constexpr void push_back(value_type&& x);
  • Effects: Moves x to the end.

  • Expects: size() < capacity().

  • Mandates: std::is_move_constructible<value_type>.

  • Complexity: Constant.

  • Remarks: If an exception is thrown by value_type's move constructor there are no effects.


constexpr void pop_back();
  • Effects: Removes the last element of the container and destroys it.

  • Expects: !empty().

  • Complexity: Constant.


constexpr iterator erase(const_iterator position);
  • Effects: Removes the element at position, destroys it, and invalidates references to elements after position.

  • Expects: position in range [begin(), end()).

  • Complexity: Linear in size().

  • Remarks: If an exception is thrown by value_type's move constructor the effects are unspecified.


constexpr iterator erase(const_iterator first, const_iterator last);
  • Effects: Removes the elements in range [first, last), destroying them, and invalidating references to elements after last.

  • Expects: [first, last) in range [begin(), end()).

  • Complexity: Linear in size() and distance(first, last).

  • Remarks: If an exception is thrown by value_type's move constructor the effects are unspecified.


constexpr void swap(static_vector& x)
  noexcept(is_nothrow_swappable_v<value_type> &&
           is_nothrow_move_constructible_v<value_type>);
  • Effects: Exchanges the contents of *this with x. All references to the elements of *this and x are invalidated.

  • Complexity: Linear in size() and x.size().

  • Remarks: Shall not participate in overload resolution unless is_move_constructible_v<value_type> is true and is_swappable_v<value_type> is true

5.7 static_vector specialized algorithms

template <typename T, size_t N>
constexpr void swap(static_vector<T, N>& x, 
                    static_vector<T, N>& y)
  noexcept(noexcept(x.swap(y)));
  • Constraints: This function shall not participate in overload resolution unless is_swappable_v<T> is true.

  • Effects: As if by x.swap(y).

  • Complexity: Linear in size() and x.size().

6. Acknowledgments

The following people have significantly contributed to the development of this proposal. This proposal is based on Boost.Container's boost::container::static_vector and my extensive usage of this class over the years. As a consequence the authors of Boost.Container (Adam Wulkiewicz, Andrew Hundt, and Ion Gaztanaga) have had a very significant indirect impact on this proposal. The implementation of libc++ std::vector and the libc++ test-suite have been used extensively while prototyping this proposal, such that its author, Howard Hinnant, has had a significant indirect impact on the result of this proposal as well. The following people provided valuable feedback that influenced some aspects of this proposal: Walter Brown, Zach Laine, Rein Halbersma, and Andrzej Krzemieński. But I want to wholeheartedly acknowledge Casey Carter for taking the time to do a very detailed analysis of the whole proposal, which was invaluable and reshaped it in fundamental ways.

7. References

static_vector's People

Contributors

gnzlbg avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

static_vector's Issues

Relational operators as non-member function templates?

@rhalbersma mentioned in the std-proposal thread:

Second, you define the op== and other relational operators as friends, which means they are generated as non-member functions at namespace scope. However, all other Standard containers are defined as non-member function templates. The difference is that the latter does not accept user-defined conversions, and the former does. Also they don't have to be friends either.

The best way to declare the relational operators op== and op< is as non-member non-friend function templates, that delegate to std::equal and std::lexicographical_compare on the embedded_vector's iterator range. Then the others can be defined as usual in terms of the former.

Oversight in the wording

--- a README.md
+++ b README.md
@@ -379,7 +379,7 @@
 Changes to `sequence.reqmts.1`:

 ```diff
-The library provides four basic kinds of sequence containers: vector,
- forward_­list, list, and deque.
+The library provides five basic kinds of sequence containers: vector,
+ static_vector, forward_­list, list, and deque.
 ```

Tags are better than static member functions

The section on default initialization:

Default initialization (possible future Extension)

The size-modifying operations of the inline_vector that do not require a value also have the following analogous member functions that perform default initialization instead of value initialization:

template <typename Value, std::size_t Capacity>
struct inline_vector {
// ...
    static constexpr inline_vector default_initialized(size_t n);
    constexpr void resize_default_initialized(size_type sz);
    constexpr void resize_unchecked_default_initialized(size_type sz);
};

Alternatively, tag dispatching could be used.

prefers static member functions to tags, but tags are objectively better:

Using tags:

optional<inline_vector<T, Capacity>> ov {
    std::in_place, std::default_initialized, N
};

the inline_vector can be constructed in place without involving a copy or move, but using a static member function:

optional<inline_vector<T, Capacity>> ov {
    std::in_place, inline_vector<T, Capacity>::default_initialized(N)
};

will necessarily incur the cost of a move operation (which in particular for inline_vector will be often as expensive as a copy).

Thanks to @akrzemi1 for the discussion! (add to acknowledgements)

Specify what `N` in the complexity is

From Casey's feedback:

Many of the complexity requirements are stated as O(N), but don't specify what N is. O(Capacity) or O(size()) or O(last - first) would be better.

Ask Casey for Feedback

@CaseyCarter Since I don't have your email, I would like to ask you if you would be willing to read my proposal and give me some feedback.

In particular I am not very familiar with POCMA in practice, and I think that considering it when discussing std::vector and small_vector would be helpful.

Cannot be used in constexpr function because of missing constexpr destructor

When trying to use fixed_capacity_vector in a constexpr function in C++20. The compilation failed because fixed_capacity_vector is not a literal type because it is missing a constexpr destructor.

After marking the existing destructors constexpr and adding
copnstexpr ~type_name() = default;
to several types, the issue was solved.

Is there any chances to get those changes in the main branch?

Incorporate Andrzej feedback

  • (FIXED in 6231af9) Declarations of static_vector's copy and move constructors (in 5.1) are duplicated: either they were pasted twice or you intended another pair of constructors.

  • Description of move constructor imprecise: it says the result "should be equal" to the original, but formally vectors may not be determined to be equal if the stored value type is not comparable. Either do not define semantics and rely on these from the requirments on sequence containers (vector does this), or use semantics as specified for optional that does not refer to "being equal".

  • (FIXED in 67afb69) signatures of cbegin() and crbegin() are missing const.

  • (FIXED in 7c2d8b9) definition of swap() -- either do not provide it and rely on requirements on sequence containers (like vector does) or add a remark that it should not participate in overload resolution unless is_move_constructible_v and is_swappable_v is true.

  • (FIXED in b3a6f7a) In a number of places you have: [first, last] or [begin(), end()] to represent a range. It is incorrectly a right-closed range. Instead it should be: [first, last) and [begin(), end()).

  • (FIXED in f85efa6) "Requires" clause is now deprecated. Instead, either use "Mandates" (compiler error if expectation not met) or "Expects" (UB if expectation not met), or say "Remarks: this function shall not participate in overload resolution unless condition C is true" (i.e. requires in the concepts-lite sense)

cc @akrzemi1

Fix the specification of insert modifier

From Casey's feedback:

O(# of elements inserted) is not generally implementable for e.g. inline_vector<int, 42> v(40); v.insert(begin(), 42); O(size) elements need to be displaced.

Fix the specification of the accesors

From Casey's feedback:

Again, missing operational semantics. If your intent is to pick up the meaning of some of these operations from the general sequence container requirements - and I think that's a good idea - you need to specify somewhere early in the specification that inline_vector<T, C> meets the sequence container requirements in [sequence.reqmts] except where otherwise specified.

data() == begin() == end() is only well-defined if iterator and pointer are the same type.

Fix mistake in iterator invalidation section

Zach Laine commented:

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.

That last one is not true -- if I insert a T before the first element, I've invalidated all iterators into the vector, no? Did you meant push_back() never invalidates?

The last one should be "inserting elements at the end of an embedded_vector never invalidates iterators".

Error handling using expected<T, std::some_exception>

Ben Craig mentioned the possibility of just using expected<T, std::some_exception> for error handling. As mentioned in the Future extensions section, such an avenue can be explored with try_xxx methods, but maybe we can just change the signature of some of the methods. In any case, I think this should be a follow-up paper improving on the fundamental fixed_capacity_vector semantics, instead of trying to propose this in the first fixed_capacity_vector proposal.

What follows is my first 5-min attempt.

Methods that kind of work out nicely

  • [[nodiscard]] constexpr expected<void, std::out_of_bounds> push_back(const value_type& x);
  • [[nodiscard]] constexpr expected<iterator, std::out_of_bounds> insert(const_iterator position, const value_type& x);
  • [[nodiscard]] constexpr expected<iterator, std::out_of_bounds> insert(const_iterator position, size_type n, const value_type& x);

Methods that consume a single value.

Open question: Should these try to return the value on failure? That could look like this:

  • [[nodiscard]] constexpr expected<void, std::out_of_bounds<optional<value_type>>> push_back(value_type&& x);

  • constexpr expected<iterator, std::out_of_bounds<optional<value_type>>> insert(const_iterator position, value_type&& x);

where std::out_of_bounds<optional<T>> to be an out-of-bounds exception that optionaly stores a T since we need to handle the case where moving the value in failed, and thus we couldn't move the value out. I don't think this is worth it: either the value was moved successfully, and is thus in the vector (so .back can be used to access it), or it wasn't moved successfully, in which case it is still in the value, so users can do:

 // don't do this:
vec.insert(pos, value_type{...});
// do this instead:
auto val = value_type{...};
vec.insert(pos, std::move(val));
// if insert throws, val wasn't moved from

So I think here we could get away with the following signatures as well:

  • [[nodiscard]] constexpr expected<void, std::out_of_bounds> push_back(value_type&& x);

  • [[nodiscard]] constexpr expected<iterator, std::out_of_bounds> insert(const_iterator position, value_type&& x);

Methods that consume a compile-time fixed number of values

If we try to support returning consumed temporaries here start to get out-of-hand and probably unusable without pattern matching and destructuring:

  • template <class... Args> [[nodiscard]] constexpr expected<iterator, out_of_bounds<tuple<optional<Args...>>>> emplace(const_iterator position, Args&&... args);

  • template <class... Args> [[nodiscard]] constexpr expected<reference, out_of_bounds<tuple<optional<Args...>>>> emplace_back(Args&&... args);

I would say that we should do the same as above: users that want to preserve the inputs in case of throw should store them somewhere first, and move them in afterwards:

  • template <class... Args> [[nodiscard]] constexpr expected<iterator, out_of_bounds> emplace(const_iterator position, Args&&... args);

  • template <class... Args> [[nodiscard]] constexpr expected<reference, out_of_bounds> emplace_back(Args&&... args);

Methods that consume a run-time number of values

Dereferencing an InputIterator potentially consumes a value from the range. Throwing out-of-bounds stops insertion, so the inserted elements are stored in the vector, and the rest are still in the range, this might work out:

  • template <class InputIterator> [[nodiscard]] constexpr expected<iterator, std::out_of_bounds> insert(const_iterator position, InputIterator first, InputIterator last);

For std::initializer_list I am not sure, but one cannot move out of an initializer list, so:

  • [[nodiscard]] constexpr expected<iterator, std::out_of_bounds> insert(const_iterator position, initializer_list<value_type> il);

will just copy values out of it, so it should be fine.

Constructors

Constructors can't return expected. We can't replace them with static methods because then the fixed_capacity_vector cannot be constructed in-place, so they must be constructors:

  • fixed_capacity_vector(value_type, size), fixed_capacity_vector(begin, end)...

We can just make them throw std::out_of_range for consistency with all other methods and be done with it.

Add full() member function

I propose to add a function

constexpr bool full() const noexcept;

counterpart of empty(). Returns true if size() == capacity(). Such function is natural and useful for all containers with limited capacity.

More useful if...

We could have the equivalent of a std::vector with a small buffer optimization where we can specify the size of the small buffer; so similar to what you currently have but with no fixed capacity.

Pre/Post conditions of constructors/resize operations

From Casey's feedback:

I would remove the "it is guaranteed...implementation defined" text and simply assert the postcondition that size() == 0.

For constructors the post-conditions are either size() == 0 or size() == n and the pre-conditions n <= capacity().

  • push_back():
    • pre: size() < capacity()
    • post: auto s = v.size(); v.push_back(...); assert(v.size() == s + 1); (need to figure out a better way to state these, maybe take a look at the latest contracts paper).
  • resize(n):
    • pre: n <= capacity() (note that n is unsigned and cannot be < 0)
    • post: size() == n
  • insert(pos, rng)
    • pre: pos in [begin, end], size() + size(rng) <= capacity() (can insert empty range on full vector)
    • post: auto s = v.size(); v.insert(rng); assert(v.size() == s + size(rng))
  • pop_back()
    • pre: size() > 0
    • post: auto s = v.size(); v.pop_back(); assert(v.size() == s - 1)
  • erase(it)
    • pre: it must be in [begin, end], size() >= 0 (can erase zero elements of empty vector, tautological since size always >= 0)
  • erase(rng)
    • pre: begin(rng)/end(rng) must be in [begin, end], size >= 0 (tautological since always true)
    • post: auto s = v.size(); v.erase(rng); assert(v.size() == s - size(rng))

TODO...

Rework exception safety from Casey's Feedback

You can't provide even the basic guarantee if T's destructor can throw; I'd simply make Destructible a precondition as it is for basically the entire standard library.

I strongly agree with your decision to leave the behavior undefined upon "exceeding" the capacity. bad_alloc doesn't make sense to me here - no allocation takes place, how could it fail? I'd leave this a precondition for now and let future language support for contracts give people the behavior they want. a brief survey of the choices made in the prior art would be useful here.

Quantify code-size cost

Zach Laine wrote in the std-proposals thread:

I really want to see some real numbers on this. Please instantiate several kinds of static_vector-like things (hand-rolled struct consisting of std::array + size_t, static_vector, your implementation, etc.) in scenarios where you would expect there to be larger code size, and let's see some assembly, or at least relative counts of instructions.

Ask Jonathan Wakely for feedback

Ask @jwakely for feedback, in particular with respect to possible future Allocator extensions that might allow implementing embedded_vector by using std::vector with an Allocator.

Build failures on GCC 8

GCC 8 has some limitations with regards to conditional noexcept, it seems. For example, trying to use push_back results in:

error: cannot call member function ‘constexpr void std::experimental::fcv_detail::storage::trivial<T, Capacity>::emplace_back(Args&& ...) [<snip>]’ without object
                 noexcept(emplace_back(forward<U>(value))))
                          ~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~

This can be worked around by using noexcept(declval<fixed_capacity_vector>().emplace_back(forward<U>(value)))).

I did this in my local copy: lethal-guitar/RigelEngine@2eae5a6

I'd be happy to make a PR if desired.

Rework constructors from Casey's feedback

I don't get the description of inline_vector(). What does "it is guaranteed that no elements will be constructed unless value_type models TrivialType, in which case this guarantee is implementation defined." mean? How could an empty inline_vector have constructed elements? Is the intent that inline_vector<T, C> where T is a trivial type always contains C live objects of type T of which only size() participate in the value of the object?

explicit inline_vector(size_type n) should specify "default-initialized" or "value-initialized" elements instead of "default constructed." "throws bad_alloc if \p n > capacity()" seems to contradict the earlier statement that exceeding capacity is a precondition violation.

For inline_vector(InputIterator first, InputIterator last), the requirement you want is "value_type shall be EmplaceConstructible into *this from *first"; the current formulation misses the case that the iterator's reference type is not a reference type. You need also require that InputIterator meets the requirements for an input iterator.

You could replace the entire description of inline_vector(initializer_list<value_type> il) with "Effects: Equivalent to inline_vector(il.begin(), il.end())"

Contiguous container requirements

From Casey's feedback:

Speaking of which, I don't recall wording in the paper that asserts that specializations of inline_vector are contiguous containers per N4594 [container.requirements.general]/13; I assume that is intended.

Need to mention that inline vector is a contiguous container, and need to mention that its iterators are are contiguous iterators...

Incorporate LWG feedback

LWG gave the following feedback on the second revision. I'll open new issues to tackle controversial change. I have tried to call out UNCLEAR and NON ACTIONABLE requests. A ticked checkbox indicates that the issue has been fixed on the master branch:

General comments

  • UNCLEAR: Exceeding the container capacity (e.g. on push_back): in revision 2 this is a pre-condition violation. Is LWG recommending that this should std::abort instead? The notes are unclear. CHANGE: added a "Note to LWG" about why LEWG agreed that this should be a pre-condition violation.

  • All Requires that cannot be checked at compile-time should be Expects

  • UNCLEAR: LWG recommended moving this into the <vector> header. LEWG's intent was to make the static_vector component freestanding. CHANGE: add changes required to make <static_vector> header freestanding, and add note to LWG indicating that currently, none of the existing freestanding headers is a good fit for static_vector.

  • UNCLEAR: "the design should be fixed" it is unclear to which part of the design this final comment refers to. Also it is unclear whether the next iteration of this proposal should be sent to.

  • UNCLEAR: what is the difference between Mandates and Requires ?

Section 5.1

  • 5.1.ii: states "meets all requirements of containers", and then gives exceptions. Instead, it should contain a sentence per container category (container, reversible container, sequence container, and contiguous container), where each sentence list the limitations for each category.

  • The note about incomplete types is redundant and should be removed.

  • difference_type has to use ptrdiff_t instead of make_signed<size_t>

  • Remove conditional noexcept() on copy-constructors and copy-assignment. Our general policy is to only use conditional noexcept on moves and swaps. Going against the general policy requires discussion and approval of the rationale in LEWG and (ideally) a recorded poll of LEWG's approval in the paper so LWG knows that discussion and approval occurred. This was not discussed in LEWG, so the conditional noexcept on these are removed.

  • INVALID: There are no deduction guides. The reviewers realized that the capacity cannot be deduced.

  • Instead of using: DefaultInserted, CopyInsertable, EmplaceConstructible, ... which require an allocator, these proposal should directly use, e.g., std::is_..._constructible instead.

    • UNCLEAR: DefaultInserted can become value initialized. RESOLVED: DefaultInserted is no longer mentioned.
  • Add static_vector to the tables of library and freestanding headers.

  • Add static_vector to the exceptions in the container requirement tables for array.

Section 5.2 Constructors

  • Spell out move constructor - reviewers mentioned that it is unclear for example if it needs to preserve the elements.

    From the proposal revision 2, Section 4.5 Move Semantics:

    The move semantics of static_vector<T, Capacity> are equal to those of std::array<T, Size>

    static_vector a(10);
    static_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: this behavior differs from std::vector<T, Allocator>, in particular for the similar case in which std::allocator_traits<Allocator>::propagate_on_container_move_assignment is false. In this situation the state of std::vector is initialized but unspecified.

  • The iterator constructor complexity should read: "Complexity: Linear in distance(first, last)."

  • Iterator constructor; change: "value_type shall be EmplaceConstructible into *this from *first and distance(first, last) <= capacity()" to:

    • Mandates: std::is_constructible<value_type, decltype(*first)>
    • Expects: distance(first, last) <= capacity()

5.3 Destructor

  • Effects has to state that the elements are destroyed.

  • Change the Requires clause to: "This destructor is trivial if the destructor of value_type is trivial.".

5.4 Size and capacity

  • Get rid of the semicolons at the end of each line (EDIT: this breaks markdown syntax highlighting).

  • Replace "Effects: equivalent to return N;." to "Returns: N."

  • Fix c++-artefact near the resize overloads.

  • Each of the resize overloads should have its own section.

  • The remarks of the resize overloads are wrong:

    • UNCLEAR: Not all constexpr types are trivially copyable. What is a "constexpr type" ? What would be the right constraint?
    • Removed the remark. The overloads are now constexpr, nothing is said about when this is the case.
  • Resize overloads: the is_default_constructible_v is only needed for the resize(size_type sz) overload

5.6 Modifies

  • Replace "Requires: The number of elements to be inserted shall be at most C - size()." with "Expects: The number of elements to be inserted shall be at most capacity() - size()."

  • The modifiers need to be split into different sections. For insert we need to care about exception behavior, but for push_back we don't. So at least two different sections for insert and push_back.

  • UNCLEAR: Throws doesn't apply here

  • UNCLEAR: push_back() or append at the end should be strong exception safe. Which part of the specification makes this not clear? The Remarks specifically states that this is the case.

  • UNCLEAR: The text seems to be copied from something allocating which doesn't apply here. Which part of the text does not apply here? For example, if the modifiers are implemented such that the size() of the container is modified before calling, e.g, a move/copy constructor/assignment to insert something, and it throws, then there will be effects (the vector will have an incorrect size()). So this needs to somehow call out that such an implementation is illegal.

  • "anassignment" should be "an assignment"

  • UNCLEAR: The Complexity is wrong. Unclear what is wrong about the complexity. Should it say "number of elements inserted plus the distance from the insertion point to the end of the static_vector." instead of "linear in the number of ...". ?

  • Split sections of pop_back() and erase() .

  • UNCLEAR "the complexity [of pop back and/or erase] doesn't make any sense"

  • UNCLEAR "the note doesn't make any sense"

    • UNCLEAR: The Note explains why these methods can't be noexcept. This is correct, that is what the note does. It is a note intended for the reader of the proposal, not for standard wording. Should I remove all the notes that are intended for the reader in the next version of the proposal ?
  • Swap member function should take a reference.

  • Complexity of swap is incorrect. It was suggested to change it to "Complexity: number of the elements exchanged" but also suggested that "not all elements are exchanged". Therefore, I propose to change it to "Complexity: min(size(), other.size()) swaps and max(size(), other.size()) - min(size(), other.size()) move constructions." or similar.

  • noexcept of member swap is wrong as there might be different number of elements in the containers. It should probably be changed from noexcept(is_nothrow_swappable_v<value_type>) to noexcept(is_nothrow_swappable_v<value_type> && is_nothrow_move_constructible_v<value_type>).

5.7 specialized algorithms

  • Replace Remarks with Constraints

cc @mclow @brevzin @dietmarkuehl @jwakely @CaseyCarter


Note to self: once I finish incorporating all the feedback in the proposal it will be time to update the implementation to match the proposal.

Incorporate LEWG feedback

The feedback from Kona is that:

  • (no change necessary): push_back should be UB when the capacity is exceeded
  • (no change necessary): free-standing header
  • (no change necessary, already fixed on master): "The copy/move constructor is duplicated. cbegin, cend do not have const overloads. Has member and non-member swap."

So AFAICT there is nothing to do here, except for summarizing the meeting notes for LWG. In particular, I'll add the following to the changelog at the top of the document:

  • note LEWG decisions about push_back and free-standing in the changelog
  • include concern in the changelog about the inability to handle errors due to UB
  • include polls about whether this should go in C++20 or in Library Fundamentals v3

The recommendation seems to be to forward the revised document to LWG, I will do it for the next mailing (or if I make it, post-meeting mailing):


@tituswinters @AlisdairM I think there was a concern raised by @AlisdairM about whether this can be replaced with a vector and a stack allocator. It is unclear to me whether this resolved itself during the discussion, or whether this issue is still open and I should raise this concern to LWG.

size_type should be size_t

size_type should be size_t, the type used to storage the vector size is the one that should be "as small as possible".

Safe for production use?

Thanks for this library, I think it's a great idea to add a static_vector type to the C++ standard.
Is the current version of the implementation reasonably safe for production use?
I'd like to use it, as it has no external dependencies like boost etc.

Improve comparison with std::vector/array

From Casey's feedback (about POCMA):

the move behavior of inline_vector is element-wise. I think I'd avoid any discussion of allocators - which few people understand anyway - and focus on which of the semantics are "more like std::vector" and which are "more like std::array".

Describing the move behavior as element-wise (like std::array) is a very clear way of explaining the semantics (as opposed to std::vector, where the ownership of memory is moved, but not the elements themselves, unless POCMA...).

Rework noexcept from Casey's feedback

Add section on noexcept, classify all functions in two groups, functions with wide or narrow contracts (without/with preconditions). Functions with wide contracts (no preconditions) can be noexcept, those with narrow contracts should have either strong arguments for making them noexcept, or not be noexcept.

The included CI script doesn't actually run any tests

Invoking run.sh in a fresh clone of the repo results in the following output:

Scanning dependencies of target test.headers
[  0%] Built target test.headers
Scanning dependencies of target tests
[ 50%] Build all the unit tests.
[ 50%] Built target tests
Scanning dependencies of target check
[100%] Build and then run all the tests and examples.
Test project /home/niko/static_vector/build
No tests were found!!!
[100%] Built target check

Add discussion about why I used conditionally noexcept/constexpr in the first revision

@Rein Halbersma wrote:

Indeed, the reference to was meant to illustrate that it was not necessary to make the comparison operators friends, but that they could be done through the public iterator interface (whether from or hand-implemented to be constexpr).

Note that if you would have a std::array as back-end for this embedded_vector, then you might want to check
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0031r0.html which has greatly expanded the constexpr-ness of std::array, but with the exception of the op==/op< and swap/fill operations. The rationale being:

Currently comparisons and swap/fill may be implemented with the help of algorithms from header. Marking comparisons with constexpr will break that ability and will potentially lead to performance degradations.

A related proposal to make mostly constexpr http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0202r1.html has not yet been adopted, mainly for concern on intrinsics and C helpers (memset and friends).

So while I would certainly encourage you to push constexpr-ness as a feature of your proposal, I would not expect this to be adopted without discussion.

Thanks for these remarks. I've added a github issue to expand the design rationale of constexpr with mentions of those two paper. How I see it, is that if those papers are accepted, the implementation of the proposal might be greatly simplified. But if they are not accepted, the proposal can still be implemented as is; doing so just has a higher implementation cost.

I think some aspects of the proposal are controversial. In particular, the "conditionally noexcept interface" and the constexpr features. I actually do not know if the proposal should have these features since there are a lot of trade-offs at play. The features are however implementable, and they do bring significant benefit in some use cases. I am assuming that the proposal won't get accepted at its first revision, but rather that it will at least be revised a couple of times. I thought that for a first revision of the proposal it would be better to add these features to show that they are implementable, that they bring benefit, and so that people can "play" with them and see how they behave or find issues. If it turns out that they should be removed in a second revision, then that's the way it is. But removing them is easy, and maybe they can be pursued in future proposals that are followups to the one you mentioned.

I should write this down in the paper to explain why I used these features. I do not want the reader to be surprised with a "noexcept/constexpr everywhere! that is not how the standard library does things!". What I actually want is to inform the reader that it is possible to do this. That doing so adds benefits, and that those benefits come with some specification and implementation costs. I think the benefits outweigh the costs (and I try to make a case for this), but I want the reader to be able to form its own opinion about this so that the discussion at the committee can be a constructive one.

Next step

@CaseyCarter no idea what the next step is. To which WG should I submit this for the next meeting ?

Compilation fails, "changing the meaning of aligned_storage_t"

The following code does not compile on my system because it is "changing the meaning of aligned_storage_t"

using aligned_storage_t
    = aligned_storage_t<sizeof(remove_const_t<T>),
                                      alignof(remove_const_t<T>)>;
using data_t = conditional_t<!Const<T>, aligned_storage_t,
                                              const aligned_storage_t>;

Changing it to the following code fixes the issue for me

using aligned_storage_t_
    = aligned_storage_t<sizeof(remove_const_t<T>),
                                      alignof(remove_const_t<T>)>;
using data_t = conditional_t<!Const<T>, aligned_storage_t_,
                                              const aligned_storage_t_>;

Casey's feedback round 2

@CaseyCarter I fixed most of the issues you mentioned in the last commit but these ones remain open:

  • constexpr has a run-time cost value initialization up to capacity() for trivial types.
  • go through container requirements, scan where array is mentioned as an exception, and consider adding "and fixed_capacity_vector".
  • implementation of range constructor, insert, and emplace so that it is constexpr for trivial Ts and only requires EmplaceConstructible (the prototype value initializes all elements and then assigns to the from the range)
  • destructor cannot be implicitly generated (the prototype does this, but maybe it is wrong?)
  • is is_trivial the right trigger to select live object implementation? (the first version of the proposal used is_literal_t but I cannot recall why I changed it)

I don't know how to fix them yet, but will try to do so before the next deadline.

__builtin_unreachable ( ) on Windows

On Windows the function void __builtin_unreachable ( ) is undefined, and therefor the code is not functional with cl.exe. The below code snippet makes the code compile and does the 'right' thing:

#if defined( _WIN32 ) && !( defined( __clang__ ) || defined( __GNUC__ ) )
#pragma warning( disable : 4645 ) // suppress warning that non-returning function returns.
    [[noreturn]] static void __builtin_unreachable ( ) noexcept { return; /* intentional */ }
#pragma warning( default : 4645 )
#endif

Please add the above to master.

PS: I decided to wrap the library and fix the problem at the same time, also, as soon as C++20 is available, I can just delete the header and the STL will kick in automatically everywhere.

// MIT License
//
// Copyright (c) 2020 degski
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#pragma once

// <static_vector>

#if defined( _WIN32 ) && !( defined( __clang__ ) || defined( __GNUC__ ) )
#    pragma warning( disable : 4645 ) // suppress warning that non-returning function returns.
    [[noreturn]] static void __builtin_unreachable ( ) noexcept { return; /* intentional */ }
#    pragma warning( default : 4645 )
#endif

#include "experimental/fixed_capacity_vector"

namespace std {
    template<typename Type, std::size_t Size>
    using static_vector = experimental::fixed_capacity_vector<Type, Size>;
}

[Question]: array of elements of union type

Have you considered using an underlying array containing elements of union type for the static_vector implementation?

Fwiiw I implemented my own StaticVector< T, N, A > with the following goals:

  • usable in constexpr expressions (unlike EASTL's eastl::fixed_vector);
  • not relying on operator new/dynamic memory allocation to support compile-time initialization and runtime usage (unlike C++20's std::vector);
  • not requiring value types to be default constructible and/or trivially destructible (unlike wrapping a std::array/C-style array).

I was inspired by std::optional< T > to achieve the latter.

One cannot use an array of std::optional< T > elements directly, because besides wrapping a union, std::optional< T > needs to keep track of the active union member as well. This bookkeeping is redundant for each array element. Furthermore, std::optional< T > has a larger size than the value it wraps, which does not allow for a data() member method.

Instead my StaticVector< T, N, A > holds an array of unions, and uses the size() member method to keep track of all active union members in the array using the following class invariants:

  • The elements at [0, size()) have the value_type;
  • The elements at [size(), capacity()) have a trivial dummy type.

A slightly stripped down version can be found at https://developercommunity.visualstudio.com/t/failure-was-caused-by-attempting-to-acce/1515298 (MSVC has some issue(s) unlike clang and gcc).

data() on an empty vector

data() on a vector of zero size but non-zero capacity:

  • implementation returns the correct buffer pointer
  • proposal does not impose any requirements
  • This matches the precedent set by std::vector: wording leaves it unspecified, vendors in practice return the buffer pointer.
  • I'd like to see the proposal require that the buffer pointer is returned (i.e. bring into line with the implementation rather than leaving it underspecified).

data() on a vector of zero capacity:

  • implementation returns nullptr
  • proposal does not impose any requirements
  • This matches the precedent set by std::vector: wording leaves it unspecified, vendors in practice return nullptr.
  • Therefore this seems fine to me. (However, it would not be bad to require that nullptr be returned, rather than leaving it underspecified.)

Update container requirements

@CaseyCarter suggested:

go through container requirements, scan where array is mentioned as an exception, and consider adding "and static_vector".

Tagging as revision 3 milestone.

Consider third template parameter to configure error semantics on precondition violation

Nicolai Josuttis wrote (talking about how to error when e.g. push_back is executed on a fixed_capacity_vector that is already full):

How about adding a third optional template argument?
With the default being to throw.
(experts are good enough experts to be able to disable overflows).
And the optional template argument should be an enum not a bool
(where we can specify that implementations may add other modes).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.