Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
stateless_allocator.hpp
Go to the documentation of this file.
1/* Flow-IPC: Shared Memory
2 * Copyright 2023 Akamai Technologies, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the
5 * "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in
12 * writing, software distributed under the License is
13 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14 * CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing
16 * permissions and limitations under the License. */
17
18/// @file
19#pragma once
20
23#include <flow/util/util.hpp>
24#include <memory>
25#include <type_traits>
26
27namespace ipc::shm::stl
28{
29
30// Types.
31
32/**
33 * Stateless allocator usable with STL-compliant containers to store (or merely read) them directly in SHM in
34 * a given SHM-aware `Arena`. Please read background in shm::stl namespace doc header (stl_fwd.hpp).
35 *
36 * ### How to use ###
37 * Suppose `T` is a container type with `Allocator` template param given as `Stateless_allocator<E>`, as well
38 * as any nested element types that are also containers also specifying `Allocator` as `Stateless_allocator<...>`.
39 * Suppose you want to work with a `T* t`, such that `t` points to `sizeof(T)` bytes in SHM inside some #Arena
40 * `A`. In order to perform any work on the `*t` -- including construction, destruction, and any other
41 * operations that require internal alloc/dealloc/ptr-deref also inside `A` -- activate `&A` in the relevant
42 * thread using an Arena_activator. For example:
43 *
44 * ~~~
45 * using Shm_classic_arena = classic::Pool_arena;
46 * template<typename T>
47 * using Shm_classic_allocator = Stateless_allocator<T, Shm_classic_arena>; // Or use Shm_classic_arena::Allocator.
48 * using Shm_classic_arena_activator = Arena_activator<Shm_classic_arena>; // Or use Shm_classic_arena::Activator.
49 *
50 * struct Widget { float m_a; int m_b; };
51 * using Widget_list = list<Widget, Shm_classic_allocator<Widget>>;
52 * using Widget_list_vec = vector<Widget_list, Shm_classic_allocator<Widget_list>>;
53 *
54 * Shm_classic_arena a{...}; // Let `a` be a SHM-classic arena storing *x among other things.
55 *
56 * Widget_list_vec* x = // ...obtain pointer x to SHM-stored space for the Widget_list_vec in question.
57 * {
58 * Shm_classic_arena_activator arena_ctx{&a};
59 * // call x-> ctors, dtor, methods, operators....
60 * } // Return the active arena to previous value.
61 * ~~~
62 *
63 * Note you may nest Arena_activator contexts in stack-like fashion. Note, also, that `Arena_activator<A1>`
64 * operates completely independently from `Arena_activator<A2>`, even in the same thread, if `A1` is not same-as
65 * `A2`. So any "clash" would only occur (1) in a given thread, not across threads; (2) for a given SHM-provider
66 * type, not across 2+ such types.
67 *
68 * The allocator is *stateless*. It takes no space within any container that uses it; and it is always default-cted
69 * by the container code itself -- never by the user. That is the reason Arena_activator is necessary (when
70 * allocating/deallocating).
71 *
72 * ### `Arena` type requirements (for full capabilities) ###
73 * For write+allocate+deallocate capabilities of Stateless_allocator: an `Arena` must have the following members:
74 * - `void* allocate(size_t n);`: Allocate uninitialized buffer of `n` bytes in this SHM arena;
75 * return locally-dereferenceable pointer to that buffer. Throw exception if ran out of resources.
76 * (Some providers try hard to avoid this.) Must not return null: Stateless_allocator uses the result unchecked.
77 * The buffer must be aligned at least as `std::max_align_t` (like `malloc()`'s result). (Corollary: value types
78 * with stricter alignment requirements are not supported; Stateless_allocator::allocate() `static_assert()`s
79 * this.)
80 * - `void deallocate(void* p)`: Undo `allocate()` that returned `p`; or the equivalent operation
81 * if the SHM-provider allows process 2 to deallocate something that was allocated by process 1 (and `p` indeed
82 * was `allocate()`ed in a different process but transmitted to the current process; and was properly made
83 * locally-dereferenceable before passing it to this method).
84 * Must not throw. Stateless_allocator::deallocate() is `noexcept` (as STL-compliant allocators' deallocation
85 * must be, as it runs during destruction and stack unwinding); so a throw here would `std::terminate()`.
86 * - `template<typename T> class Pointer`: `Pointer<T>` must, informally speaking, mimic `T*`; formally
87 * being a *fancy pointer* in the STL sense. The locally-dereferenceable `T*` it yields must point to the
88 * same underlying area in SHM regardless of in which `Arena`-aware process the deref API is invoked.
89 * - The `class` keyword is for exposition only; it can also be `using`, `typedef`, or anything else,
90 * provided it mimics `T*` as described.
91 * - For example, if #Arena is classic::Pool_arena, then classic::Pool_arena::Pointer might be
92 * `bipc::offset_ptr`, which internally stores merely an offset within the same SHM-pool versus its own
93 * `this`; this works great as long as indeed the `Pointer` *itself* is located inside the same SHM-pool as the
94 * thing to which it points.
95 *
96 * @todo Currently `Arena::Pointer` shall be a fancy-pointer, but we could support raw pointers also.
97 * Suppose #Arena is set up in such a way as to map all processes' locally-dereferenceable pointers to the same SHM
98 * location to the same numeric value (by specifying each pool's start as some predetermined numerical value in
99 * the huge 64-bit vaddr space) in all processes sharing that SHM pool. Now no address translation is
100 * needed, and `Arena::Pointer` could be simply `T*`. As of this writing some inner impl details assume
101 * it being a fancy-pointer, and it's the only active need we have; but that could be tweaked with a bit of effort.
102 *
103 * ### Use in read-only borrowing mode ###
104 * Taken as an example, SHM-classic (shm::classic::Pool_arena) `Arena`s can always both read and write, allocate and
105 * deallocate. By contrast, shm::arena_lend has allocate/write-capable `Arena` and, on the borrower side,
106 * read-only borrower-quasi-`Arena`. If one needs to merely *read* a SHM-stored STL-compliant structure with such
107 * an `Arena`, then **only the `Pointer` type requirement above applies**.
108 * - `allocate()` and `deallocate()` shall not be used by (nor need to be present at compile-time for)
109 * Stateless_allocator.
110 * - No activator -- in fact, no `Arena` *object* -- only the *type*! -- shall be used by Stateless_allocator.
111 *
112 * @internal
113 *
114 * ### Implementation ###
115 * It is self-explanatory; the trick was knowing what was actually required according to STL-compliance documentation.
116 * The great thing is, since C++11, only very few things are indeed needed in our situation; the rest is
117 * supplied with sensible defaults by `allocator_traits` which is how STL-compliant container code actually accesses
118 * `Allocator`s like ours.
119 *
120 * @endinternal
121 *
122 * @tparam T
123 * Pointed-to type for the allocator. See standard C++ `Allocator` concept.
124 * @tparam Arena_t
125 * See above + #Arena.
126 */
127template<typename T, typename Arena_t>
129{
130public:
131 // Types.
132
133 /// Alias for `T`.
134 using Value = T;
135
136 /// Alias for the `Arena_t` type this uses for allocation/deallocation/pointer semantics.
137 using Arena = Arena_t;
138
139 /// The required pointer-like type. See also #pointer.
140 using Pointer = typename Arena::template Pointer<Value>;
141
142 /// Alias to #Pointer for compatibility with STL-compliant machinery (traits, etc.).
144
145 /// Alias to #Value for compatibility with STL-compliant machinery (traits, etc.).
147
148 /**
149 * Signed type for differences between #Pointer values, for compatibility with STL-compliant machinery (traits,
150 * etc.). This is the default `std::allocator_traits` would use anyway, if this alias were not here.
151 *
152 * ### Rationale ###
153 * Why define it (and #size_type) then? 1, is it dangerous + 2, why do it regardless? Answer 1: Not dangerous: as
154 * `allocator_traits` would choose it anyway as noted. Answer 2: Doing it because some real-life container types,
155 * due to slightly not-quite-following standard requirements, will refuse as of this writing to compile
156 * when using us as the allocator with #Arena = `Ipc_arena` from SHM-jemalloc. Specifically:
157 * `boost::container::allocator_traits` does not follow the standard defaults: if the allocator lacks these aliases,
158 * it uses `std::ptrdiff_t` and `std::size_t`, regardless of #Pointer. Meanwhile some boost.container code
159 * (e.g., `deque` internals) as of this writing also uses the `Pointer`-derived types, mixing the two. So
160 * if #Pointer `difference_type` is narrower than `std::ptrdiff_t` -- as with SHM-jemalloc's `Shm_pool_offset_ptr` --
161 * such code can fail to compile (ambiguous overloads). Defining these two aliases keeps both sources
162 * consistent.
163 *
164 * So do not remove these as seemingly redundant.
165 */
166 using difference_type = typename std::pointer_traits<Pointer>::difference_type;
167
168 /// Unsigned counterpart of #difference_type, for compatibility with STL-compliant machinery; see #difference_type.
169 using size_type = std::make_unsigned_t<difference_type>;
170
171 // Constructors/destructor.
172
173 /// Allocator concept requirement for default-ctor: no-op since it's a stateless allocator.
175
176 /**
177 * Allocator concept requirement for copy-ctor from allocator-to-another-type: no-op since it's a stateless
178 * allocator.
179 *
180 * @tparam U
181 * Like #Value.
182 * @param src_ignored
183 * The other allocator.
184 */
185 template<typename U>
186 explicit Stateless_allocator(const Stateless_allocator<U, Arena>& src_ignored);
187
188 /**
189 * Allocator concept requirement for move-ctor from allocator-to-another-type: no-op since it's a stateless
190 * allocator.
191 *
192 * @tparam U
193 * Like #Value.
194 * @param src_ignored
195 * The other allocator.
196 */
197 template<typename U>
199
200 // Methods.
201
202 /**
203 * Allocates an uninitialized buffer of given size, or throws exception if `Arena::allocate()` does;
204 * satisfies formal requirements of STL-compliant `Allocator` concept. See cppreference.com for those formal
205 * requirements.
206 *
207 * @param n
208 * The buffer allocated shall be sized `n * sizeof(Value)` bytes.
209 * Note: `n` is a #Value count; not a byte count.
210 * @return Locally-dereferenceable pointer to the SHM-allocated buffer.
211 * The buffer is *not* initialized. E.g., depending on the nature of #Value you may want to placement-ct it
212 * (`construct_at()`) at this address subsequently.
213 */
214 Pointer allocate(size_t n) const;
215
216 /**
217 * Deallocates buffer in SHM previously allocated via allocate() or equivalent in this or other (if #Arena
218 * supports this) process, as long as `p` refers to the beginning of the buffer returned by that `allocate()`;
219 * satisfies formal requirement of STL-compliant `Allocator` concept. See cppreference.com for those formal
220 * requirements. Does not throw (as required).
221 *
222 * @param p
223 * See above.
224 * @param n_ignored
225 * Ignored.
226 */
227 void deallocate(Pointer p, size_t n_ignored = 0) const noexcept;
228}; // class Stateless_allocator
229
230// Free functions: in *_fwd.hpp.
231
232// Template implementations.
233
234template<typename T, typename Arena_t>
235typename Stateless_allocator<T, Arena_t>::Pointer Stateless_allocator<T, Arena_t>::allocate(size_t n) const
236{
237 static_assert(alignof(Value) <= flow::util::max_align_sz(),
238 "Arena::allocate() takes no alignment arg and guarantees only std::max_align_t alignment; "
239 "over-aligned value types are not supported.");
240
242 assert(arena && "Before working with SHM-stored STL-compliant objects: activate an Arena via Arena_activator "
243 "in the thread in question.");
244
245 /* void* => Value* => Arena::Pointer<Value>. The last => is a key, non-trivial operation that creates
246 * the SHM-storable fancy-pointer from a locally-dereferenceable raw pointer. Though typically the fancy-pointer
247 * template Arena::Pointer<> would have a ctor that takes a Value*, officially in STL-compliant land it's
248 * the static pointer_to() factory. pointer_traits<>::pointer_to(T&) does that for non-raw
249 * pointers per cppreference.com and also yields a simple &x for raw pointer types (also correct).
250 *
251 * arena->allocate() in particular can throw -- for example Pool_arena::allocate() throws a `bad_alloc` on
252 * running out of pre-configured SHM-pool space. Propagating it is correct: per cppreference.com
253 * - Allocator::allocate() "may throw exceptions";
254 * - there is no specific mandatory exception. */
255 return std::pointer_traits<Pointer>::pointer_to
256 (*(static_cast<Value*>
257 (arena->allocate(n * sizeof(Value))))); // May throw.
258}
259
260template<typename T, typename Arena_t>
262{
264 assert(arena && "Before working with SHM-stored STL-compliant objects: activate an Arena via Arena_activator "
265 "in the thread in question.");
266
267 /* Arena::Pointer<Value> => Value* => void*. The first => is a key, non-trivial operation that obtains
268 * a locally-dereferenceable raw pointer from the fancy-pointer. This is expressible generically in a number
269 * of ways; but for any fancy-pointer type `.operator->()` will do it.
270 * @todo In C++20 std::to_address() would invoke that for fancy-pointer and simply pass-through the T*
271 * for raw pointer. If we wanted to allow raw pointer support then we'd have to either wait for C++20 and
272 * use that helper; or implement it in C++17 ourselves (see cppreference.com for inspiration). */
273 arena->deallocate(static_cast<void*>(p.operator->()));
274}
275
276template<typename T, typename Arena_t>
278
279template<typename T, typename Arena_t>
280template<typename U>
282{
283 // As usual... do nothin'... there's no state.
284}
285
286template<typename T, typename Arena_t>
287template<typename U>
289{
290 // As usual... do nothin'... there's no state.
291}
292
293template<typename Arena_t, typename T1, typename T2>
295{
296 static_assert(std::is_empty_v<Stateless_allocator<T1, Arena_t>>,
297 "Stateless_allocator<> is currently designed around being empty (static-data-only) -- "
298 "did it gain state?");
299 return true;
300}
301
302template<typename Arena_t, typename T1, typename T2>
304{
305 return false;
306}
307
308} // namespace ipc::shm::stl
static Arena * this_thread_active_arena()
Returns the active arena, as understood by Stateless_allocator<T, Arena> (any T) at this point in the...
Stateless allocator usable with STL-compliant containers to store (or merely read) them directly in S...
Pointer pointer
Alias to Pointer for compatibility with STL-compliant machinery (traits, etc.).
Stateless_allocator()
Allocator concept requirement for default-ctor: no-op since it's a stateless allocator.
Pointer allocate(size_t n) const
Allocates an uninitialized buffer of given size, or throws exception if Arena::allocate() does; satis...
Arena_t Arena
Alias for the Arena_t type this uses for allocation/deallocation/pointer semantics.
Value value_type
Alias to Value for compatibility with STL-compliant machinery (traits, etc.).
bool operator==(const Stateless_allocator< T1, Arena_t > &val1, const Stateless_allocator< T2, Arena_t > &val2)
Returns true for any 2 Stateless_allocators managing the same Stateless_allocator::Arena.
void deallocate(Pointer p, size_t n_ignored=0) const noexcept
Deallocates buffer in SHM previously allocated via allocate() or equivalent in this or other (if Aren...
bool operator!=(const Stateless_allocator< T1, Arena_t > &val1, const Stateless_allocator< T2, Arena_t > &val2)
Returns false for any 2 Stateless_allocators managing the same Stateless_allocator::Arena.
std::make_unsigned_t< difference_type > size_type
Unsigned counterpart of difference_type, for compatibility with STL-compliant machinery; see differen...
typename Arena::template Pointer< Value > Pointer
The required pointer-like type. See also pointer.
typename std::pointer_traits< Pointer >::difference_type difference_type
Signed type for differences between Pointer values, for compatibility with STL-compliant machinery (t...
ipc::shm sub-module providing integration between STL-compliant components (including containers) and...