D3418R0
Wording for P3160R2 inplace_vector’s "construct/destroy" alternative

Draft Proposal,

Author:
Audience:
LEWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Draft Revision:
15

Abstract

P3160R2 proposes to make inplace_vector an allocator-aware container. It offers two design alternatives, nicknamed "uses-allocator" and "construct/destroy," and presents wording for the "uses-allocator" alternative. This paper (P3418) provides wording for the "construct/destroy" alternative. This paper also provides additional motivation for allocator-awareness.

1. Changelog

2. Motivation and proposal

[P0843] introduced inplace_vector<T, N> to the C++26 working draft. [P3160R1] proposes that inplace_vector should support an allocator parameter, thus allowing it to use that allocator’s construct and destroy members, even though it never needs to allocate.

(1) This permits inplace_vector to interoperate with allocator-extended client code, thus making it a better drop-in replacement for vector. For example:

template<class Ctr, class... Args>
Ctr f(Args... args) {
  using Alloc = typename Ctr::allocator_type;
  Alloc alloc(args...);
  return Ctr(alloc);
}
auto v1 = f<std::vector<int>>();
 
auto v3 = f<std::pmr::vector<int>>(&mr);
auto v1 = f<std::vector<int>>();
auto v2 = f<std::inplace_vector<int, 3>>();
auto v3 = f<std::pmr::vector<int>>(&mr);

(2) This permits using inplace_vector with a PMR allocator. The vector’s elements are still allocated directly in-place inside the memory footprint of the inplace_vector object, but the elements in turn are constructed with allocator-extended construction. Without allocator-awareness, the code is cumbersome and error-prone. Notice that neither snippet below ever allocates from the global heap.

std::pmr::set_default_resource(nullptr);
char buf[10000];
std::pmr::monotonic_buffer_resource mr(buf, 10000);
std::inplace_vector<std::pmr::string, 5> pv;
const char *contents[] = {
  "A string so long that it requires heap allocation",
  "A short string",
};
for (const char *p : contents) {
  pv.emplace_back(p, &mr);
}
pv.emplace_back("A string so long that it requires heap allocation", &mr);
std::pmr::inplace_vector<std::pmr::string, 5> pv(&mr);
const char *contents[] = {
  "A string so long that it requires heap allocation",
  "A short string",
};
pv.insert(contents.begin(), contents.end());
pv.emplace_back("A string so long that it requires heap allocation");

(3) This permits using inplace_vector with a Boost.Interprocess allocator, with the same benefits as the PMR example above: less code to write, and fewer chances to write bugs by accident. See "Boost.Interprocess, and sg14::inplace_vector" (2024-08-23).

namespace bip = boost::interprocess;
namespace shm {
  template<class T> using allocator = std::scoped_allocator_adaptor<bip::allocator<T, bip::managed_shared_memory::segment_manager>>;
  template<class T> using vector = std::vector<T, shm::allocator<T>>;
  using string = boost::container::basic_string<char, std::char_traits<char>, shm::allocator<char>>;
}
auto segment = bip::managed_shared_memory(bip::create_only, SHM_SEGMENT_NAME, 10000);
auto alloc = shm::allocator<int>(segment.get_segment_manager());
namespace shm {
  template<class T>
  using vector = std::vector<T, shm::allocator<T>>;
}
using VecOfStrings = shm::vector<shm::string>;
auto *p = segment.construct<VecOfStrings>("S")(argv+2, argv+argc, alloc);
namespace shm {
  template<class T, size_t N>
  using inplace_vector = std::inplace_vector<T, N, shm::allocator<T>>;
}
using IPVOfStrings = shm::inplace_vector<shm::string, 5>;
auto *p = segment.construct<IPVOfStrings>("S")(argv+2, argv+argc, alloc);
using IPVOfStrings = std::inplace_vector<shm::string, 5>;
auto *p = segment.construct<IPVOfStrings>("S")();
for (int i = 2; i < argc; ++i) {
  p->emplace_back(argv[i], alloc);
}
using IPVOfStrings = shm::inplace_vector<shm::string, 5>;
auto *p = segment.construct<IPVOfStrings>("S")(argv+2, argv+argc, alloc);
 
 
 

(4) This gives the programmer control over the size_type and difference_type members of inplace_vector, permitting a predictably smaller (or larger) memory footprint. Now, STL vendors could implement memory-footprint optimizations to store the size member in fewer bits when N was small; but we have seen from our experience with variant that vendors do not do such optimizations in practice, and then get locked out of them forever by ABI. Giving control to the programmer is more general and reduces the STL vendor’s own workload. (Godbolt.)

       









static_assert(sizeof(std::inplace_vector<char, 3>) == 16);
namespace Tiny {
  template<class T>
  struct Alloc {
    using value_type = T;
    using size_type = uint8_t;
    static T* allocate(size_t) { assert(false); }
    static void deallocate(T*) { assert(false); }
  };
  template<class T, size_t N>
  using inplace_vector = std::inplace_vector<T, N, Tiny::Alloc<T>>;
}
static_assert(sizeof(std::inplace_vector<char, 3>) == 16);
static_assert(sizeof(Tiny::inplace_vector<char, 3>) == 4);

Note: Mital Ashok’s draft implementation of inplace_vector for libc++ ([Mital]) does store the size in a reduced footprint, based on the absolute value of N. That is, we have "reduced the STL vendor’s workload" in an area where libc++ has already done that work! libc++ also uses a reduced footprint for variant’s index in its "unstable ABI" mode. This proposal doesn’t forbid an implementation like Mital’s; libc++ is free to continue using a reduced footprint even after this proposal. But certainly argument (4) in favor is weak: the space-saving is superfluous (in libc++'s case) and non-portable (since different STL vendors may continue to choose different types for the size data member regardless of Allocator::size_type). From the user-programmer’s point of view, it matters only that there is some way to store the size member in fewer than eight bytes: either every STL vendor commits to store size in the minimum possible number of bytes, or this proposal is adopted and every STL vendor commits to store size in a type no larger than Allocator::size_type.

2.1. Notes on freestanding

inplace_vector<T, N> is freestanding. We don’t propose to change that. But now it’s a synonym for inplace_vector<T, N, allocator<T>>. So, allocator<T> needs to become freestanding. Freestanding implementations lack a heap, so allocator<T>::allocate becomes a freestanding-deleted member function ([freestanding.item]/3), which means that it may be =delete’d if not provided by a freestanding implementation. This is fine because inplace_vector doesn’t use allocate or deallocate.

allocator_traits is already freestanding in C++23, thanks to Ben Craig’s [P1642]. But it is UB to instantiate allocator_traits<A> unless A meets the allocator requirements ([allocator.requirements]/3), and the allocator requirements require that A::allocate must be present!

We could solve this conundrum in any of the following ways:

Arthur drafted wording for the third option, but Pablo suggests taking a variation on that approach that basically anticipates Ben Craig’s [P3295] — marking allocate and deallocate with a new "freestanding-consteval" category — except that instead of copying large portions of [P3295] into this paper, we simply achieve the same effect with a Note. If [P3295] is adopted, we can easily update our Note to use the freestanding-consteval markup instead.

2.2. Notes on feature-test macros

For the non-freestanding case, it’s easy: vendors just bump __cpp_lib_inplace_vector and are done. This subsection exists only because the freestanding situation is messier than that, because we are making allocator partially freestanding.

This situation falls under [P2198]'s subsection labeled "Detect C++20 (or older) feature that is now partially required in freestanding." Ben Craig, in email, suggests this four-macro design.

Macro Meaning
__cpp_lib_inplace_vector (exists) If 202406L, P0843 is fully implemented; if YYYYMML, allocator-aware inplace_vector is fully implemented
__cpp_lib_freestanding_inplace_vector (new) At least the freestanding subset of inplace_vector is available (but e.g. the freestanding-deleted iterator-pair constructor might be =deleted)
__cpp_lib_allocator (new) std::allocator is fully implemented (including allocate and deallocate)
__cpp_lib_freestanding_allocator (new) At least the freestanding subset of std::allocator is available (but e.g. allocate might be =deleted)

Note: The freestanding-deleted members, such as the iterator-pair constructor, are freestanding-deleted not because they allocate (they don’t) but because they throw.

The existing feature-test macros related to allocator support on freestanding are C++23’s __cpp_lib_allocate_at_least (the last major change to allocator/allocator_traits), C++26’s __cpp_lib_freestanding_memory, and C++26’s __cpp_lib_freestanding_operator_new (effectively indicates whether heap allocation is available).

2.3. Notes on allocator propagation

The STL uses allocators both for allocation/deallocation and for construction/destruction. So the happy path for STL containers is when the allocator sticks alongside both the value of the data pointer (which must be deallocated) and the lifetimes of the element objects (which must be destroyed).

Containers that store their elements on the heap want POCCA/POCMA/POCS to be true: they’ll simply swap the allocator alongside the data pointer. When POCCA/POCMA/POCS is false, those containers are unhappy: their constant-time move operations become linear-time and throwing, and their swap (which refuses to become throwing) requires that the two allocators be equal, on pain of UB.

inplace_vector wants POCCA/POCMA/POCS to be false! Each element of v1 is itself an object that was constructed via allocator_traits<A>::construct(a1, ...) and must eventually be destroyed by allocator_traits<A>::destroy(a1, ...). Therefore, as long as v1 has any elements, v1 must never forget about a1. When POCCA/POCMA/POCS is false, this is easy and natural. When POCCA/POCMA/POCS is true, inplace_vector is unhappy: its copy-assignment, move-assignment, and swap require for correctness that the two allocators be equal, on pain of UB.

For an example involving CountingAlloc<T>, see this Godbolt.

Operation Heap-storage vector inplace_vector
Move construction Non-allocating and O(1) Non-allocating and O(n)
Copy construction O(n) non-allocating and O(n)
Move assignment Non-allocating and O(1) by default. If !POCMA and !is_always_equal, it becomes allocating and O(n) Non-allocating and O(n). If POCMA and !is_always_equal, it may have UB
Copy assignment O(n) Non-allocating and O(n). If POCCA and !is_always_equal, it may have UB
Swap Non-allocating, noexcept, and O(1). If !POCS and !is_always_equal, it may have UB Non-allocating and O(n). If POCS and !is_always_equal, it may have UB

We follow the Lakos rule and make each special member function noexcept(false) whenever it may have UB.

Theoretically, we could special-case the preconditions to make it not UB to swap/assign two vectors with unequal propagating allocators if either vector is empty — because then there’s no insurmountable physical difficulty with CountingAlloc. However, that idea would complicate the spec, make more work for vendors, and make the UB condition less legible to users. So we don’t do that.

2.3.1. Notes on preconditions

For swap in particular, even if we assume the "construct/destroy" model instead of the "uses-allocator" model, Arthur and Pablo disagree as to what would be the appropriate precondition w.r.t. allocator equality. Arthur’s proposed wording is as follows. The first part of the Precondition element is modeled on [alg.swap]/2 (as used by array::swap), and the second part (the part dealing with allocators) is modeled on [container.reqmts]/65.

constexpr void swap(inplace_vector& x) noexcept(see below);

x․ Preconditions: Let M be min(size(), x.size()). For each non-negative integer n < M, (*this)[n] is swappable with ([swappable.requirements]) x[n]. If allocator_traits<allocator_type>::propagate_on_container_swap::value is true, then allocator_type meets the Cpp17Swappable requirements and get_allocator() == x.get_allocator().

x․ Effects: [...]

Pablo would prefer something more like this:

constexpr void swap(inplace_vector& x) noexcept(see below);

x․ Preconditions: Let a1 and a2 be get_allocator() and x.get_allocator(), respectively; p1 and p2 be pointers of type T* to storage suitable to hold an object of type T; and args1 and args2 be function parameter packs such that T(args1...) and T(args2...) are well-formed and well-defined. The following shall be well-formed and well-defined:

allocator_traits<allocator_type>::construct(a1, p1, args1...);
allocator_traits<allocator_type>::construct(a2, p2, args2...);
using std::swap;
swap(*p1, *p2);
if constexpr (allocator_traits<allocator_type>::propagate_on_container_swap::value)
  swap(a1, a2);
allocator_traits<allocator_type>::destroy(a1, p1);
allocator_traits<allocator_type>::destroy(a2, p2);

[Note: In other words, when propagate_on_container_swap::value is false, it must be valid to swap the individual elements even when the allocators do not compare equal. Conversely, when propagate_on_container_swap::value is true, it must be valid to destroy the swapped elements using the swapped allocators, even though the call to destroy uses a different allocator than the corresponding call to construct. —end note]

x․ Effects: [...]

We encourage discussion as to which direction LEWG prefers.

Arthur’s precondition has the benefit of being checkable at runtime, because it involves the values of computable expressions like get_allocator() == x.get_allocator(). An implementation would be allowed to assert-fail if the precondition was violated, using something like SG14’s SG14_INPLACE_VECTOR_ASSERT_PRECONDITION macro or libc++'s _LIBCPP_ASSERT_COMPATIBLE_ALLOCATOR macro.

Pablo’s precondition is not checkable at runtime, but has the benefit of being looser: it permits the user to swap inplace_vectors with allocators that are "unequal, yet functionally interchangeable." The inplace_vector implementation would be forbidden to assert-fail inside operator= or swap.

It is not possible to violate either precondition with std::allocator (which is always equal) nor with std::pmr::polymorphic_allocator (which doesn’t propagate).

[LWG4151] is related: inplace_vector::swap is already missing a precondition today. Both of the above-proposed preconditions are supersets of LWG4151’s proposed resolution.

2.4. Notes on noexceptness

We expect that in practice, STL vendors will take approximately the same approach to noexceptness which sg14::inplace_vector has taken. This approach is:

static constexpr bool CopyCtorIsNoexcept =
    ((std::is_nothrow_copy_constructible_v<T> &&
      sg14::aaipv::has_trivial_construct<Alloc, T, const T&>::value) || (N == 0)) &&
    sg14::aaipv::propagate_on_container_copy_construction<Alloc>::value;

static constexpr bool MoveCtorIsNoexcept =
    ((std::is_nothrow_move_constructible_v<T> &&
      sg14::aaipv::has_trivial_construct<Alloc, T, T&&>::value) || (N == 0));

static constexpr bool CopyAssignIsNoexcept =
    ((std::is_nothrow_copy_constructible_v<T> &&
      std::is_nothrow_copy_assignable_v<T> &&
      std::is_nothrow_destructible_v<T> &&
      sg14::aaipv::has_trivial_construct<Alloc, T, const T&>::value &&
      sg14::aaipv::has_trivial_destroy<Alloc, T>::value) || (N == 0)) &&
    (std::allocator_traits<Alloc>::is_always_equal::value || !std::allocator_traits<Alloc>::propagate_on_container_copy_assignment::value);

static constexpr bool MoveAssignIsNoexcept =
    ((std::is_nothrow_move_constructible_v<T> &&
      std::is_nothrow_move_assignable_v<T> &&
      std::is_nothrow_destructible_v<T> &&
      sg14::aaipv::has_trivial_construct<Alloc, T, T&&>::value &&
      sg14::aaipv::has_trivial_destroy<Alloc, T>::value) || (N == 0)) &&
    (std::allocator_traits<Alloc>::is_always_equal::value || !std::allocator_traits<Alloc>::propagate_on_container_move_assignment::value);

static constexpr bool SwapIsNoexcept =
    ((std::is_nothrow_swappable_v<T> &&
      std::is_nothrow_move_constructible_v<T> &&
      sg14::aaipv::has_trivial_construct<Alloc, T, T&&>::value &&
      sg14::aaipv::has_trivial_destroy<Alloc, T>::value) || (N == 0)) &&
     (std::allocator_traits<Alloc>::is_always_equal::value || !std::allocator_traits<Alloc>::propagate_on_container_swap::value);

Here sg14::aaipv::propagate_on_container_copy_construction<Alloc> is true iff select_on_container_copy_construction is defaulted to simply copy the allocator (which cannot throw); sg14::aaipv::has_trivial_construct<Alloc, T, Args...> is true iff construct(p, args...) is defaulted to simply call std::construct(p, args...); and sg14::aaipv::has_trivial_destroy<Alloc, T> is true iff destroy(p) is defaulted to simply call std::destroy_at(p). STL vendors might reasonably choose to further specialize these helpers for std::allocator and std::pmr::polymorphic_allocator.

However, for wording purposes, we can’t use these helpers because they aren’t standardized: we don’t have any easy way to invoke them in the wording. So our proposed wording is written as if each helper trait always yields false, but in a way that (we intend) permits the STL vendor to strengthen the noexcept guarantee — and we hope that they will do so!

Note: [res.on.exception.handling]/5 permits STL vendors only to add noexcept to function signatures that completely lack it; it doesn’t permit vendors to strengthen noexcept(A) into noexcept(A || B). This is unfortunate.

Our exception-specifications don’t mention the copy/move/equality operations on Alloc itself, because [allocator.requirements] forbids these operations to throw (on pain of UB).

3. Implementation experience

Arthur has implemented § 5 Proposed wording in [QuuxplusoneSG14] (see Godbolt). This includes extensive unit tests verifying that:

4. Acknowledgments

Thanks to Pablo Halpern for [P3160R1] and [P3160R2]. Thanks to Ben Craig for his design of the feature-test macros and for discussion of the approach to "freestanding allocator." Thanks to Gonzalo Brito and Mital Ashok for their review of this design and wording. Thanks to Casey Carter for his review of this wording.

5. Proposed wording

In this section, wording in strikethrough is to be deleted; wording in green is to be added no matter which alternative we go with; wording in yellow is to be added only in the "construct/destroy alternative"; wording in blue is to be added only in the "uses-allocator alternative."

5.1. [version.syn]

Modify [version.syn] as follows, inserting these macros into the list in the proper alphabetical positions:

#define __cpp_lib_allocator                    YYYYMML // also in <memory>
#define __cpp_lib_freestanding_allocator       YYYYMML // freestanding, also in <memory>
#define __cpp_lib_inplace_vector               202406L // also in <inplace_vector>
#define __cpp_lib_inplace_vector               YYYYMML // also in <inplace_vector>
#define __cpp_lib_freestanding_inplace_vector  YYYYMML // freestanding, also in <inplace_vector>

5.2. [memory.syn]

Modify [memory.syn] as follows:

// [default.allocator], the default allocator
template<class T> class allocator;                                              // partially freestanding
template<class T, class U>
  constexpr bool operator==(const allocator<T>&, const allocator<U>&) noexcept; // freestanding

5.3. [default.allocator]

Modify [default.allocator] as follows:

namespace std {
  template<class T> class allocator {
  public:
    using value_type                             = T;
    using size_type                              = size_t;
    using difference_type                        = ptrdiff_t;
    using propagate_on_container_move_assignment = true_type;

    constexpr allocator() noexcept;
    constexpr allocator(const allocator&) noexcept;
    template<class U> constexpr allocator(const allocator<U>&) noexcept;
    constexpr ~allocator();
    constexpr allocator& operator=(const allocator&) = default;

    constexpr T* allocate(size_t n);                              // Note 1
    constexpr allocation_result<T*> allocate_at_least(size_t n);  // Note 1
    constexpr void deallocate(T* p, size_t n);                    // Note 1
  };
}

x․ [Note: For a freestanding implementation, it is implementation-defined whether allocate, allocate_at_least, and deallocate are consteval rather than constexpr. —end note]

2․ allocator_traits<allocator<T>>::is_always_equal::value is true for any T.

5.4. [container.alloc.reqmts]

Modify [container.alloc.reqmts] as follows:

1․ Except for array and inplace_vector, all of the containers defined in [containers], [stacktrace.basic], [basic.string], and [re.results] meet the additional requirements of an allocator-aware container, as described below.

2․ Given an allocator type A and given a container type X having a value_type identical to T and an allocator_type identical to allocator_traits<A>::rebind_alloc<T> and given an lvalue m of type A, a pointer p of type T*, an expression v that denotes an lvalue of type T or const T or an rvalue of type const T, and an rvalue rv of type T, the following terms are defined. If X is a specialization of inplace_vector, the terms below are defined as if A were scoped_allocator_adaptor<allocator<T>, allocator_type>. If X is not allocator-aware or is a specialization of basic_string, the terms below are defined as if A were allocator<T> — no allocator object needs to be created and user specializations of allocator<T> are not instantiated:

(2.1) — T is Cpp17DefaultInsertable into X means that the following expression is well-formed:

allocator_traits<A>::construct(m, p)

(2.2) — An element of X is default-inserted if it is initialized by evaluation of the expression

allocator_traits<A>::construct(m, p)
where p is the address of the uninitialized storage for the element allocated within X.

(2.3) — T is Cpp17MoveInsertable into X means that the following expression is well-formed:

allocator_traits<A>::construct(m, p, rv)
and its evaluation causes the following postcondition to hold: The value of *p is equivalent to the value of rv before the evaluation. [Note:rv remains a valid object. Its state is unspecified. —end note]

(2.4) — T is Cpp17CopyInsertable into X means that, in addition to T being Cpp17MoveInsertable into X, the following expression is well-formed:

allocator_traits<A>::construct(m, p, v)
and its evaluation causes the following postcondition to hold: The value of v is unchanged and is equivalent to *p.

(2.5) — T is Cpp17EmplaceConstructible into X from args, for zero or more arguments args, means that the following expression is well-formed:

allocator_traits<A>::construct(m, p, args)

(2.6) — T is Cpp17Erasable from X means that the following expression is well-formed:

allocator_traits<A>::destroy(m, p)
[Note: A container calls allocator_traits<A>::construct(m, p, args) to construct an element at p using args, with m == get_allocator() (or m == A(allocator<T>(), get_allocator()) in the case of inplace_vector). The default construct in allocator will call ::new((void*)p) T(args), but specialized allocators can choose a different definition. —end note]

[...]

A type X meets the allocator-aware container requirements if X meets the container requirements and the following types, statements, and expressions are well-formed and have the specified semantics.

typename X::allocator_type

4․ Result: A

5․ Mandates: allocator_type::value_type is the same as X::value_type.

c.get_allocator()

6․ Result: A

7․ Complexity: Constant.

X u;
X u = X();

8․ Preconditions: A meets the Cpp17DefaultConstructible requirements.

9․ Postconditions: u.empty() returns true, u.get_allocator() == A().

10․ Complexity: Constant.

X u(m);

11․ Postconditions: u.empty() returns true, u.get_allocator() == m.

12․ Complexity: Constant.

X u(t, m);

13․ Preconditions: T is Cpp17CopyInsertable into X.

14․ Postconditions: u == t, u.get_allocator() == m

15․ Complexity: Linear.

X u(rv);

16․ Postconditions: u has the same elements as rv had before this construction; the value of u.get_allocator() is the same as the value of rv.get_allocator() before this construction.

17․ Complexity: Constant.

X u(rv, m);

18․ Preconditions: T is Cpp17MoveInsertable into X.

19․ Postconditions: u has the same elements, or copies of the elements, that rv had before this construction, u.get_allocator() == m.

20․ Complexity: Constant if m == rv.get_allocator(), otherwise linear.

a = t

21․ Result: X&.

22․ Preconditions: T is Cpp17CopyInsertable into X and Cpp17CopyAssignable.

23․ Postconditions: a == t is true.

24․ Complexity: Linear.

a = rv

25․ Result: X&.

26․ Preconditions: If allocator_traits<allocator_type>::propagate_on_container_move_assignment::value is false, T is Cpp17MoveInsertable into X and Cpp17MoveAssignable.

27․ Effects: All existing elements of a are either move assigned to or destroyed.

28․ Postconditions: If a and rv do not refer to the same object, a is equal to the value that rv had before this assignment.

29․ Complexity: Linear.

a.swap(b)

30․ Result: void

31․ Effects: Exchanges the contents of a and b.

32․ Complexity: Constant.

5.5. [inplace.vector.syn]

Modify [inplace.vector.syn] as follows:

24.3.7 Header <inplace_vector> synopsis [inplace.vector.syn]

// mostly freestanding
#include <compare>              // see [compare.syn]
#include <initializer_list>     // see [initializer.list.syn]
 
namespace std {

  // exposition-only type traits
  template<class T, class A, class... X>
    constexpr bool is-nothrow-ua-constructible-v = see below; // exposition only

// [inplace.vector], class template inplace_vector template<class T, size_t N, class Allocator> class inplace_vector; // partially freestanding   // [inplace.vector.erasure], erasure template<class T, size_t N, class Allocator, class U = T> constexpr typename inplace_vector<T, N, Allocator>::size_type erase(inplace_vector<T, N, Allocator>& c, const U& value); template<class T, size_t N, class Allocator, class Predicate> constexpr typename inplace_vector<T, N, Allocator>::size_type erase_if(inplace_vector<T, N, Allocator>& c, Predicate pred);  

namespace pmr { template<class T, size_t N> using inplace_vector = std::inplace_vector<T, N, polymorphic_allocator<T>>; } }

5.6. [inplace.vector]

Modify [inplace.vector] as follows:

24.3.14 Class template inplace_vector [inplace.vector]

24.3.14.1 Overview [inplace.vector.overview]

1․ An inplace_vector is a contiguous container. Its capacity is fixed and its elements are stored within the inplace_vector object itself. [Note: Although it is an allocator-aware container, inplace_vector uses its allocator only to construct and destroy allocator-aware elements; it never directly instantiates the allocate or deallocate member functions of its allocator_type.— end note]

2․ An inplace_vector meets all of the requirements of a container ([container.reqmts]), of a reversible container ([container.rev.reqmts]), of an allocator-aware container ([container.alloc.reqmts]), of a contiguous container, and of a sequence container, including most of the optional sequence container requirements ([sequence.reqmts]). The exceptions are the push_front, prepend_range, pop_front, and emplace_front member functions, which are not provided. Descriptions are provided here only for operations on inplace_vector that are not described in one of these tables or for operations where there is additional semantic information.

3․ For any N, inplace_vector<T, N, allocator<T>>::iterator and inplace_vector<T, N, allocator<T>>::const_iterator meet the constexpr iterator requirements.

4․ For any N > 0 and any Allocator, if is_trivial_v<T> is false, then no inplace_vector<T, N, Allocator> member functions are usable in constant expressions.

5․ Any member function of inplace_vector<T, N, Allocator> that would cause the size to exceed N throws an exception of type bad_alloc.

6․ Let IV denote a specialization of inplace_vector<T, N> inplace_vector<T, N, allocator<T>> inplace_vector<T, N, A>. If N is zero, then IV is empty. If N is zero and either is_trivial_v<A> is true or A is a specialization of allocator, then IV is both trivial and empty. Otherwise:

  • (6.1) If is_trivially_copy_constructible_v<T> && is_trivially_copy_constructible_v<A> is true and A::select_on_container_copy_construction does not exist, then IV has a trivial copy constructor.
  • (6.2) If is_trivially_move_constructible_v<T> && is_trivially_move_constructible_v<A> is true, then IV has a trivial move constructor.
  • (6.3) If is_trivially_destructible_v<T> is true, then:
    • (6.3.1) If is_trivially_destructible_v<A> is true, then IV has a trivial destructor.
    • (6.3.2) If is_trivially_copy_constructible_v<T> && is_trivially_copy_assignable_v<T> && is_trivially_copy_assignable_v<A> && (allocator_traits<A>::propagate_on_container_copy_assignment::value || allocator_traits<A>::is_always_equal::value) is true, then IV has a trivial copy assignment operator.
    • (6.3.3) If is_trivially_move_constructible_v<T> && is_trivially_move_assignable_v<T> && is_trivially_move_assignable_v<A> && (allocator_traits<A>::propagate_on_container_move_assignment::value || allocator_traits<A>::is_always_equal::value) is true, then IV has a trivial move assignment operator.

    7․ The exposition-only trait is-nothrow-ua-constructible-v<T, A, X...> is true if uses-allocator construction with an allocator of type A and constructor arguments of types specified by X... ([allocator.uses.construction]) is known to be a non-throwing operation. [Note: This trait can be implemented by instantiating is_nothrow_constructible_v<T, Y...>, where Y... is the set of tuple arguments deduced by uses_allocator_construction_args. —end note]

    namespace std {
      template<class T, size_t N, class Allocator = allocator<T>>
      class inplace_vector {
      public:
        // types:
        using value_type             = T;
        using allocator_type         = Allocator;
        using pointer                = T* typename allocator_traits<Allocator>::pointer;
        using const_pointer          = const T* typename allocator_traits<Allocator>::const_pointer;
        using reference              = value_type&;
        using const_reference        = const value_type&;
        using size_type              = size_t implementation-defined; // see [container.requirements]
        using difference_type        = ptrdiff_t implementation-defined; // see [container.requirements]
        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>;
     
        // [inplace.vector.cons], construct/copy/destroy
        constexpr inplace_vector() noexcept(noexcept(Allocator())) : inplace_vector(Allocator()) { };
        constexpr explicit inplace_vector(const Allocator&) noexcept;
        constexpr explicit inplace_vector(size_type n,
                                          const Allocator& = Allocator());      // freestanding-deleted
        constexpr inplace_vector(size_type n, const T& value,
                                 const Allocator& = Allocator());               // freestanding-deleted
        template<class InputIterator>
          constexpr inplace_vector(InputIterator first, InputIterator last,
                                   const Allocator& = Allocator());             // freestanding-deleted
        template<container-compatible-range<T> R>
          constexpr inplace_vector(from_range_t, R&& rg,
                                   const Allocator& = Allocator());             // freestanding-deleted
        constexpr inplace_vector(const inplace_vector&);
        constexpr inplace_vector(inplace_vector&&)
          noexcept(N == 0 || is_nothrow_move_constructible_v<T>)
          noexcept(N == 0 || is-nothrow-ua-constructible-v<T, Allocator, T&&>)
          noexcept(see below);
        constexpr inplace_vector(const inplace_vector& rhs, const type_identity_t<Allocator>&);
        constexpr inplace_vector(inplace_vector&& rhs, const type_identity_t<Allocator>&);
        constexpr inplace_vector(initializer_list<T> il,
                                 const Allocator& = Allocator());               // freestanding-deleted
        constexpr ~inplace_vector();
        constexpr inplace_vector& operator=(const inplace_vector& other rhs);
        constexpr inplace_vector& operator=(inplace_vector&& other rhs)
          noexcept(N == 0 || (is_nothrow_move_assignable_v<T> &&
                              is_nothrow_move_constructible_v<T>))
          noexcept(N == 0 || (is_nothrow_move_assignable_v<T> &&
                              is-nothrow-ua-constructible-v<T, Allocator, T&&>))
          noexcept(see below);
        constexpr inplace_vector& operator=(initializer_list<T>);               // freestanding-deleted
        template<class InputIterator>
          constexpr void assign(InputIterator first, InputIterator last);       // freestanding-deleted
        template<container-compatible-range<T> R>
          constexpr void assign_range(R&& rg);                                  // freestanding-deleted
        constexpr void assign(size_type n, const T& u);                         // freestanding-deleted
        constexpr void assign(initializer_list<T> il);                          // freestanding-deleted
        constexpr allocator_type get_allocator() const noexcept;
     
        // 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;
     
        // [inplace.vector.capacity] 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);                                    // freestanding-deleted
        constexpr void resize(size_type sz, const T& c);                        // freestanding-deleted
        static constexpr void reserve(size_type n);                             // freestanding-deleted
        static constexpr void shrink_to_fit() noexcept;
     
        // element access
        constexpr reference       operator[](size_type n);
        constexpr const_reference operator[](size_type n) const;
        constexpr reference       at(size_type n);                              // freestanding-deleted
        constexpr const_reference at(size_type n) const;                        // freestanding-deleted
        constexpr reference       front();
        constexpr const_reference front() const;
        constexpr reference       back();
        constexpr const_reference back() const;
     
        // [inplace.vector.data], data access
        constexpr       T* data()       noexcept;
        constexpr const T* data() const noexcept;
     
        // [inplace.vector.modifiers], modifiers
        template<class... Args>
          constexpr reference emplace_back(Args&&... args);                     // freestanding-deleted
        constexpr reference push_back(const T& x);                              // freestanding-deleted
        constexpr reference push_back(T&& x);                                   // freestanding-deleted
        template<container-compatible-range<T> R>
          constexpr void append_range(R&& rg);                                  // freestanding-deleted
        constexpr void pop_back();
     
        template<class... Args>
          constexpr pointer T* try_emplace_back(Args&&... args);
        constexpr pointer T* try_push_back(const T& x);
        constexpr pointer T* try_push_back(T&& x);
        template<container-compatible-range<T> R>
          constexpr ranges::borrowed_iterator_t<R> try_append_range(R&& rg);
     
        template<class... Args>
          constexpr reference unchecked_emplace_back(Args&&... args);
        constexpr reference unchecked_push_back(const T& x);
        constexpr reference unchecked_push_back(T&& x);
     
        template<class... Args>
          constexpr iterator emplace(const_iterator position, Args&&... args);  // freestanding-deleted
        constexpr iterator insert(const_iterator position, const T& x);         // freestanding-deleted
        constexpr iterator insert(const_iterator position, T&& x);              // freestanding-deleted
        constexpr iterator insert(const_iterator position, size_type n,         // freestanding-deleted
                                  const T& x);
        template<class InputIterator>
          constexpr iterator insert(const_iterator position,                    // freestanding-deleted
                                    InputIterator first, InputIterator last);
        template<container-compatible-range<T> R>
          constexpr iterator insert_range(const_iterator position, R&& rg);     // freestanding-deleted
        constexpr iterator insert(const_iterator position,                      // freestanding-deleted
                                  initializer_list<T> il);
        constexpr iterator erase(const_iterator position);
        constexpr iterator erase(const_iterator first, const_iterator last);
        constexpr void swap(inplace_vector& x)
          noexcept(N == 0 || (is_nothrow_swappable_v<T> &&
                              is_nothrow_move_constructible_v<T>))
          noexcept(N == 0 || (is_nothrow_swappable_v<T> &&
                              is-nothrow-ua-constructible-v<T, Allocator, T&&>))
          noexcept(see below);
        constexpr void clear() noexcept;
     
        constexpr friend bool operator==(const inplace_vector& x,
                                         const inplace_vector& y);
        constexpr friend synth-three-way-result<T>
          operator<=>(const inplace_vector& x, const inplace_vector& y);
        constexpr friend void swap(inplace_vector& x, inplace_vector& y)
          noexcept(N == 0 || (is_nothrow_swappable_v<T> &&
                              is_nothrow_move_constructible_v<T>))
          noexcept(noexcept(x.swap(y)))
          { x.swap(y); }
      };
    };
    

    24.3.14.2. Constructors [inplace.vector.cons]

    DRAFTING NOTE: The use of explicit on inplace_vector(n, alloc) is consistent with the other STL containers (e.g. vector and list).

    constexpr explicit vector(const Allocator&);
    

    x․ Effects: Constructs an empty inplace_vector, using the specified allocator.

    x․ Complexity: Constant.

    constexpr explicit inplace_vector(size_type n, const Allocator& = Allocator());
    

    1․ Preconditions: T is Cpp17DefaultInsertable into inplace_vector.

    2․ Effects: Constructs an inplace_vector with n default-inserted elements, using the specified allocator.

    3․ Complexity: Linear in n.

    constexpr inplace_vector(size_type n, const T& value,
                             const Allocator& = Allocator());
    

    4․ Preconditions: T is Cpp17CopyInsertable into inplace_vector.

    5․ Effects: Constructs an inplace_vector with n copies of value, using the specified allocator.

    6․ Complexity: Linear in n.

    template<class InputIterator>
      constexpr inplace_vector(InputIterator first, InputIterator last,
                               const Allocator& = Allocator());
    

    7․ Effects: Constructs an inplace_vector equal to the range [first, last), using the specified allocator.

    8․ Complexity: Linear in distance(first, last).

    template<container-compatible-range<T> R>
      constexpr inplace_vector(from_range_t, R&& rg, const Allocator& = Allocator());
    

    9․ Effects: Constructs an inplace_vector object with the elements of the range rg, using the specified allocator.

    10․ Complexity: Linear in ranges::distance(rg).

    constexpr inplace_vector(inplace_vector&&)
      noexcept(N == 0 || is-nothrow-ua-constructible-v<T, Allocator, T&&>);
      noexcept(see below);
    

    11․ Preconditions: T is Cpp17MoveInsertable into inplace_vector.

    12․ Complexity: Linear.

    13․ Remarks: When allocator_type is allocator<value_type>, the exception specification is equivalent to:

    is_nothrow_move_constructible_v<value_type>
    
    Otherwise, the exception specification is unspecified.

    constexpr inplace_vector& operator=(const inplace_vector& rhs);
    

    14․ Preconditions: allocator_traits<allocator_type>::propagate_on_container_copy_assignment::value is false or rhs.get_allocator() == this->get_allocator(). T is Cpp17CopyInsertable into inplace_vector and Cpp17CopyAssignable.

    constexpr inplace_vector& operator=(inplace_vector&& rhs)
      noexcept(N == 0 || (is_nothrow_move_assignable_v<T> &&
                          is-nothrow-ua-constructible-v<T, Allocator, T&&>));
      noexcept(see below);
    

    15․ Preconditions: allocator_traits<allocator_type>::propagate_on_container_move_assignment::value is false or rhs.get_allocator() == this->get_allocator(). T is Cpp17MoveInsertable into inplace_vector and Cpp17MoveAssignable.

    16․ Complexity: Linear.

    17․ Remarks: When allocator_type is allocator<value_type>, the exception specification is equivalent to:

    is_nothrow_move_assignable_v<value_type> && is_nothrow_move_constructible_v<value_type>
    
    Otherwise, the exception specification is unspecified.

    DRAFTING NOTE: type_identity_t below is consistent with the other STL containers. CTAD needs it in order to handle this snippet: std::pmr::inplace_vector<int,10> v; auto w = inplace_vector(v, &mr);

    constexpr inplace_vector(const inplace_vector& rhs, const type_identity_t<Allocator>&);
    

    1․ Effects: Constructs an inplace_vector with the elements of rhs, using the specified allocator.

    2․ Complexity: Linear in rhs.size().

    constexpr inplace_vector(inplace_vector&& rhs, const type_identity_t<Allocator>&);
    

    3․ Effects: Constructs an inplace_vector with the elements that rhs had before the construction, using the specified allocator.

    4․ Complexity: Linear in rhs.size().

    24.3.14.3 Size and capacity [inplace.vector.capacity]

    static constexpr size_type capacity() noexcept;
    static constexpr size_type max_size() noexcept;
    

    1․ Returns: N.

    constexpr void resize(size_type sz);
    

    2․ Preconditions: T is Cpp17DefaultInsertable into inplace_vector.

    3․ Effects: If sz < size(), erases the last size() - sz elements from the sequence. Otherwise, appends sz - size() default-inserted elements to the sequence.

    4․ Remarks: If an exception is thrown, there are no effects on *this.

    constexpr void resize(size_type sz, const T& c);
    

    5․ Preconditions: T is Cpp17CopyInsertable into inplace_vector.

    6․ Effects: If sz < size(), erases the last size() - sz elements from the sequence. Otherwise, appends sz - size() copies of c to the sequence.

    7․ Remarks: If an exception is thrown, there are no effects on *this.

    24.3.14.4 Data [inplace.vector.data]

    constexpr       T* data()       noexcept;
    constexpr const T* data() const noexcept;
    

    1․ Returns: A pointer such that [data(), data() + size()) is a valid range. For a non-empty inplace_vector, data() == addressof(front()) is true.

    2․ Complexity: Constant time.

    24.3.14.5 Modifiers [inplace.vector.modifiers]

    constexpr iterator insert(const_iterator position, const T& x);
    constexpr iterator insert(const_iterator position, T&& x);
    constexpr iterator insert(const_iterator position, size_type n, const T& x);
    template<class InputIterator>
      constexpr iterator insert(const_iterator position, InputIterator first, InputIterator last);
    template<container-compatible-range<T> R>
      constexpr iterator insert_range(const_iterator position, R&& rg);
    constexpr iterator insert(const_iterator position, initializer_list<T> il);
     
    template<class... Args>
      constexpr iterator emplace(const_iterator position, Args&&... args);
    template<container-compatible-range<T> R>
      constexpr void append_range(R&& rg);
    

    1․ Let n be the value of size() before this call for the append_range overload, and distance(begin, position) otherwise.

    2․ Complexity: Linear in the number of elements inserted plus the distance to the end of the vector.

    3․ Remarks: If an exception is thrown other than by the copy constructor, move constructor, assignment operator, or move assignment operator of T or by any InputIterator operation, there are no effects. Otherwise, if an exception is thrown, then size() ≥ n and elements in the range begin() + [0, n) are not modified.

    constexpr reference push_back(const T& x);
    constexpr reference push_back(T&& x);
    template<class... Args>
      constexpr reference emplace_back(Args&&... args);
    

    4․ Returns: back().

    5․ Throws: bad_alloc or any exception thrown by the initialization of the inserted element.

    6․ Complexity: Constant.

    7․ Remarks: If an exception is thrown, there are no effects on *this.

    template<class... Args>
      constexpr pointer T* try_emplace_back(Args&&... args);
    constexpr pointer T* try_push_back(const T& x);
    constexpr pointer T* try_push_back(T&& x);
    

    8․ Let vals denote a pack:

    • (8.1) std::forward<Args>(args)... for the first overload,
    • (8.2) x for the second overload,
    • (8.3) std::move(x) for the third overload.

    9․ Preconditions: value_type is Cpp17EmplaceConstructible into inplace_vector from vals....

    10․ Effects: If size() < capacity() is true, appends an object of type T direct-non-list-initialized with vals.... Otherwise, there are no effects.

    11․ Returns: nullptr if size() == capacity() is true, otherwise addressof(back()).

    12․ Throws: Nothing unless an exception is thrown by the initialization of the inserted element.

    13․ Complexity: Constant.

    14․ Remarks: If an exception is thrown, there are no effects on *this.

    template<container-compatible-range<T> R>
      constexpr ranges::borrowed_iterator_t<R> try_append_range(R&& rg);
    

    15․ Preconditions: value_type is Cpp17EmplaceConstructible into inplace_vector from *ranges::begin(rg).

    16․ Effects: Appends copies of initial elements in rg before end(), until all elements are inserted or size() == capacity() is true. Each iterator in the range rg is dereferenced at most once.

    17․ Returns: An iterator pointing to the first element of rg that was not inserted into *this, or ranges::end(rg) if no such element exists.

    18․ Complexity: Linear in the number of elements inserted.

    19․ Remarks: Let n be the value of size() prior to this call. If an exception is thrown after the insertion of k elements, then size() equals n+k, elements in the range begin() + [0, n) are not modified, and elements in the range begin() + [n, n+k) correspond to the inserted elements.

    template<class... Args>
      constexpr reference unchecked_emplace_back(Args&&... args);
    

    20․ Preconditions: size() < capacity() is true.

    21․ Effects: Equivalent to: return *try_emplace_back(std::forward<Args>(args)...);

    constexpr reference unchecked_push_back(const T& x);
    constexpr reference unchecked_push_back(T&& x);
    

    22․ Preconditions: size() < capacity() is true.

    23․ Effects: Equivalent to: return *try_push_back(std::forward<decltype(x)>(x));

    static constexpr void reserve(size_type n);
    

    24․ Effects: None.

    25․ Throws: bad_alloc if n > capacity() is true.

    static constexpr void shrink_to_fit() noexcept;
    

    26․ Effects: None.

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

    27․ Effects: Invalidates iterators and references at or after the point of the erase.

    28․ Throws: Nothing unless an exception is thrown by the assignment operator or move assignment operator of T.

    29․ Complexity: The destructor of T is called the number of times equal to the number of the elements erased, but the assignment operator of T is called the number of times equal to the number of elements after the erased elements.

    constexpr void swap(inplace_vector& x)
      noexcept(N == 0 || (is_nothrow_swappable_v<T> &&
                          is-nothrow-ua-constructible-v<T, Allocator, T&&>));
    

    30․ Effects: If allocator_traits<A>::propagate_on_container_swap::value && !allocator_traits<A>::always_compare_equal::value is true, swap the allocators of *this and x. Then, swap the first m elements of *this and x, where m is the smaller of this->size() and x.size(). Then, move-insert the remaining elements from the longer inplace_vector to the end of the shorter one, then erase those elements from the longer inplace_vector.

    32․ Complexity: Linear.

    constexpr void swap(inplace_vector& x) noexcept(see below);
    

    30․ Preconditions: Let M be min(size(), x.size()). For each non-negative integer n < M, (*this)[n] is swappable with ([swappable.requirements]) x[n]. If allocator_traits<allocator_type>::propagate_on_container_swap::value is true, then allocator_type meets the Cpp17Swappable requirements and get_allocator() == x.get_allocator().

    31․ Effects: Exchanges the contents of *this and x.

    32․ Complexity: Linear.

    33․ Remarks: When allocator_type is allocator<value_type>, the exception specification is equivalent to:

    is_nothrow_swappable_v<value_type> && is_nothrow_move_constructible_v<value_type>
    
    Otherwise, the exception specification is unspecified.

    DRAFTING NOTE: The exception specification above is modeled on [optional.swap]/5.

    24.3.14.6 Erasure [inplace.vector.erasure]

    template<class T, size_t N, class Allocator, class U = T>
      constexpr size_t typename inplace_vector<T, N, Allocator>::size_type
        erase(inplace_vector<T, N, Allocator>& c, const U& value);
    

    1․ Effects: Equivalent to:

        auto it = remove(c.begin(), c.end(), value);
        auto r = distance(it, c.end());
        c.erase(it, c.end());
        return r;
    

    template<class T, size_t N, class Allocator, class Predicate>
      constexpr size_t typename inplace_vector<T, N, Allocator>::size_type
        erase_if(inplace_vector<T, N, Allocator>& c, Predicate pred);
    

    2․ Effects: Equivalent to:

        auto it = remove_if(c.begin(), c.end(), pred);
        auto r = distance(it, c.end());
        c.erase(it, c.end());
        return r;
    

References

Informative References

[LWG4151]
Arthur O'Dwyer. Precondition of inplace_vector::swap. September 2024. URL: https://cplusplus.github.io/LWG/issue4151
[Mital]
Mital Ashok. [libc++] Implement std::inplace_vector<T, N>. August 2024. URL: https://github.com/llvm/llvm-project/pull/105981
[P0843]
Gonzalo Brito Gadeschi; et al. inplace_vector. June 2024. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p0843r14.html
[P1642]
Ben Craig. Freestanding Library: Easy [utilities], [ranges], and [iterators]. July 2022. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p1642r11.html
[P2198]
Ben Craig. Freestanding Feature-Test Macros and Implementation-Defined Extensions. December 2022. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2198r7.html
[P3160R1]
Pablo Halpern. Allocator-aware inplace_vector. March 2024. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3160r1.html
[P3160R2]
Pablo Halpern. Allocator-aware inplace_vector. October 2024. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3160r2.html
[P3295]
Ben Craig. Freestanding constexpr containers and constexpr exception types. May 2024. URL: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3295r0.html
[QuuxplusoneSG14]
Arthur O'Dwyer. Quuxplusone/SG14: Allocator-aware in-place vector (future > C++20). August 2024. URL: https://github.com/Quuxplusone/SG14?tab=readme-ov-file#allocator-aware-in-place-vector-future--c20