]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- |
2 | // vim: ts=8 sw=2 smarttab | |
3 | /* | |
4 | * Ceph - scalable distributed file system | |
5 | * | |
6 | * Copyright (C) 2017 Red Hat, Inc. | |
7 | * | |
8 | * This is free software; you can redistribute it and/or | |
9 | * modify it under the terms of the GNU Lesser General Public | |
10 | * License version 2.1, as published by the Free Software | |
11 | * Foundation. See file COPYING. | |
12 | * | |
13 | */ | |
14 | ||
9f95a23c TL |
15 | #pragma once |
16 | ||
11fdf7f2 TL |
17 | #include <cstddef> |
18 | #include <utility> | |
19 | #include <type_traits> | |
20 | ||
21 | namespace ceph { | |
22 | // `static_ptr` | |
23 | // =========== | |
24 | // | |
25 | // It would be really nice if polymorphism didn't require a bunch of | |
26 | // mucking about with the heap. So let's build something where we | |
27 | // don't have to do that. | |
28 | // | |
29 | namespace _mem { | |
30 | ||
31 | // This, an operator function, is one of the canonical ways to do type | |
32 | // erasure in C++ so long as all operations can be done with subsets | |
33 | // of the same arguments (which is not true for function type erasure) | |
34 | // it's a pretty good one. | |
35 | enum class op { | |
f67539c2 | 36 | move, destroy, size |
11fdf7f2 TL |
37 | }; |
38 | template<typename T> | |
39 | static std::size_t op_fun(op oper, void* p1, void* p2) | |
40 | { | |
41 | auto me = static_cast<T*>(p1); | |
42 | ||
43 | switch (oper) { | |
11fdf7f2 TL |
44 | case op::move: |
45 | new (p2) T(std::move(*me)); | |
46 | break; | |
47 | ||
48 | case op::destroy: | |
49 | me->~T(); | |
50 | break; | |
51 | ||
52 | case op::size: | |
53 | return sizeof(T); | |
54 | } | |
55 | return 0; | |
56 | } | |
57 | } | |
58 | // The thing itself! | |
59 | // | |
60 | // The default value for Size may be wrong in almost all cases. You | |
61 | // can change it to your heart's content. The upside is that you'll | |
62 | // just get a compile error and you can bump it up. | |
63 | // | |
64 | // I *recommend* having a size constant in header files (or perhaps a | |
65 | // using declaration, e.g. | |
66 | // ``` | |
67 | // using StaticFoo = static_ptr<Foo, sizeof(Blah)>` | |
68 | // ``` | |
69 | // in some header file that can be used multiple places) so that when | |
70 | // you create a new derived class with a larger size, you only have to | |
71 | // change it in one place. | |
72 | // | |
73 | template<typename Base, std::size_t Size = sizeof(Base)> | |
74 | class static_ptr { | |
75 | template<typename U, std::size_t S> | |
76 | friend class static_ptr; | |
77 | ||
78 | // Refuse to be set to anything with whose type we are | |
79 | // incompatible. Also never try to eat anything bigger than you are. | |
80 | // | |
81 | template<typename T, std::size_t S> | |
82 | constexpr static int create_ward() noexcept { | |
83 | static_assert(std::is_void_v<Base> || | |
84 | std::is_base_of_v<Base, std::decay_t<T>>, | |
85 | "Value to store must be a derivative of the base."); | |
86 | static_assert(S <= Size, "Value too large."); | |
87 | static_assert(std::is_void_v<Base> || !std::is_const<Base>{} || | |
88 | std::is_const_v<T>, | |
89 | "Cannot assign const pointer to non-const pointer."); | |
90 | return 0; | |
91 | } | |
92 | // Here we can store anything that has the same signature, which is | |
93 | // relevant to the multiple-versions for move/copy support that I | |
94 | // mentioned above. | |
95 | // | |
96 | size_t (*operate)(_mem::op, void*, void*); | |
97 | ||
98 | // This is mutable so that get and the dereference operators can be | |
99 | // const. Since we're modeling a pointer, we should preserve the | |
100 | // difference in semantics between a pointer-to-const and a const | |
101 | // pointer. | |
102 | // | |
103 | mutable typename std::aligned_storage<Size>::type buf; | |
104 | ||
105 | public: | |
106 | using element_type = Base; | |
107 | using pointer = Base*; | |
108 | ||
109 | // Empty | |
110 | static_ptr() noexcept : operate(nullptr) {} | |
111 | static_ptr(std::nullptr_t) noexcept : operate(nullptr) {} | |
112 | static_ptr& operator =(std::nullptr_t) noexcept { | |
113 | reset(); | |
114 | return *this; | |
115 | } | |
116 | ~static_ptr() noexcept { | |
117 | reset(); | |
118 | } | |
119 | ||
120 | // Since other pointer-ish types have it | |
121 | void reset() noexcept { | |
122 | if (operate) { | |
123 | operate(_mem::op::destroy, &buf, nullptr); | |
124 | operate = nullptr; | |
125 | } | |
126 | } | |
127 | ||
128 | // Set from another static pointer. | |
129 | // | |
130 | // Since the templated versions don't count for overriding the defaults | |
11fdf7f2 TL |
131 | static_ptr(static_ptr&& rhs) |
132 | noexcept(std::is_nothrow_move_constructible_v<Base>) : operate(rhs.operate) { | |
133 | if (operate) { | |
134 | operate(_mem::op::move, &rhs.buf, &buf); | |
135 | } | |
136 | } | |
137 | ||
11fdf7f2 TL |
138 | template<typename U, std::size_t S> |
139 | static_ptr(static_ptr<U, S>&& rhs) | |
140 | noexcept(std::is_nothrow_move_constructible_v<U>) : operate(rhs.operate) { | |
141 | create_ward<U, S>(); | |
142 | if (operate) { | |
143 | operate(_mem::op::move, &rhs.buf, &buf); | |
144 | } | |
145 | } | |
146 | ||
11fdf7f2 TL |
147 | static_ptr& operator =(static_ptr&& rhs) |
148 | noexcept(std::is_nothrow_move_constructible_v<Base>) { | |
149 | reset(); | |
150 | if (rhs) { | |
151 | operate = rhs.operate; | |
152 | operate(_mem::op::move, &rhs.buf, &buf); | |
153 | } | |
154 | return *this; | |
155 | } | |
156 | ||
11fdf7f2 TL |
157 | template<typename U, std::size_t S> |
158 | static_ptr& operator =(static_ptr<U, S>&& rhs) | |
159 | noexcept(std::is_nothrow_move_constructible_v<U>) { | |
160 | create_ward<U, S>(); | |
161 | reset(); | |
162 | if (rhs) { | |
163 | operate = rhs.operate; | |
164 | operate(_mem::op::move, &rhs.buf, &buf); | |
165 | } | |
166 | return *this; | |
167 | } | |
168 | ||
169 | // In-place construction! | |
170 | // | |
171 | // This is basically what you want, and I didn't include value | |
172 | // construction because in-place construction renders it | |
173 | // unnecessary. Also it doesn't fit the pointer idiom as well. | |
174 | // | |
175 | template<typename T, typename... Args> | |
176 | static_ptr(std::in_place_type_t<T>, Args&& ...args) | |
177 | noexcept(std::is_nothrow_constructible_v<T, Args...>) | |
178 | : operate(&_mem::op_fun<T>){ | |
179 | static_assert((!std::is_nothrow_copy_constructible_v<Base> || | |
180 | std::is_nothrow_copy_constructible_v<T>) && | |
181 | (!std::is_nothrow_move_constructible_v<Base> || | |
182 | std::is_nothrow_move_constructible_v<T>), | |
183 | "If declared type of static_ptr is nothrow " | |
184 | "move/copy constructible, then any " | |
185 | "type assigned to it must be as well. " | |
186 | "You can use reinterpret_pointer_cast " | |
187 | "to get around this limit, but don't " | |
188 | "come crying to me when the C++ " | |
189 | "runtime calls terminate()."); | |
190 | create_ward<T, sizeof(T)>(); | |
191 | new (&buf) T(std::forward<Args>(args)...); | |
192 | } | |
193 | ||
194 | // I occasionally get tempted to make an overload of the assignment | |
195 | // operator that takes a tuple as its right-hand side to provide | |
196 | // arguments. | |
197 | // | |
198 | template<typename T, typename... Args> | |
199 | void emplace(Args&& ...args) | |
200 | noexcept(std::is_nothrow_constructible_v<T, Args...>) { | |
201 | create_ward<T, sizeof(T)>(); | |
202 | reset(); | |
203 | operate = &_mem::op_fun<T>; | |
204 | new (&buf) T(std::forward<Args>(args)...); | |
205 | } | |
206 | ||
207 | // Access! | |
208 | Base* get() const noexcept { | |
209 | return operate ? reinterpret_cast<Base*>(&buf) : nullptr; | |
210 | } | |
211 | template<typename U = Base> | |
212 | std::enable_if_t<!std::is_void_v<U>, Base*> operator->() const noexcept { | |
213 | return get(); | |
214 | } | |
215 | template<typename U = Base> | |
216 | std::enable_if_t<!std::is_void_v<U>, Base&> operator *() const noexcept { | |
217 | return *get(); | |
218 | } | |
219 | operator bool() const noexcept { | |
220 | return !!operate; | |
221 | } | |
222 | ||
223 | // Big wall of friendship | |
224 | // | |
225 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
226 | friend static_ptr<U, Z> static_pointer_cast(const static_ptr<T, S>& p); | |
227 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
228 | friend static_ptr<U, Z> static_pointer_cast(static_ptr<T, S>&& p); | |
229 | ||
230 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
231 | friend static_ptr<U, Z> dynamic_pointer_cast(const static_ptr<T, S>& p); | |
232 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
233 | friend static_ptr<U, Z> dynamic_pointer_cast(static_ptr<T, S>&& p); | |
234 | ||
235 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
236 | friend static_ptr<U, Z> const_pointer_cast(const static_ptr<T, S>& p); | |
237 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
238 | friend static_ptr<U, Z> const_pointer_cast(static_ptr<T, S>&& p); | |
239 | ||
240 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
241 | friend static_ptr<U, Z> reinterpret_pointer_cast(const static_ptr<T, S>& p); | |
242 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
243 | friend static_ptr<U, Z> reinterpret_pointer_cast(static_ptr<T, S>&& p); | |
244 | ||
245 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
246 | friend static_ptr<U, Z> resize_pointer_cast(const static_ptr<T, S>& p); | |
247 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
248 | friend static_ptr<U, Z> resize_pointer_cast(static_ptr<T, S>&& p); | |
249 | }; | |
250 | ||
251 | // These are all modeled after the same ones for shared pointer. | |
252 | // | |
253 | // Also I'm annoyed that the standard library doesn't have | |
254 | // *_pointer_cast overloads for a move-only unique pointer. It's a | |
255 | // nice idiom. Having to release and reconstruct is obnoxious. | |
256 | // | |
257 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
11fdf7f2 TL |
258 | static_ptr<U, Z> static_pointer_cast(static_ptr<T, S>&& p) { |
259 | static_assert(Z >= S, | |
260 | "Value too large."); | |
261 | static_ptr<U, Z> r; | |
262 | if (static_cast<U*>(p.get())) { | |
263 | p.operate(_mem::op::move, &p.buf, &r.buf); | |
264 | r.operate = p.operate; | |
265 | } | |
266 | return r; | |
267 | } | |
268 | ||
269 | // Here the conditional is actually important and ensures we have the | |
270 | // same behavior as dynamic_cast. | |
271 | // | |
272 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
11fdf7f2 TL |
273 | static_ptr<U, Z> dynamic_pointer_cast(static_ptr<T, S>&& p) { |
274 | static_assert(Z >= S, | |
275 | "Value too large."); | |
276 | static_ptr<U, Z> r; | |
277 | if (dynamic_cast<U*>(p.get())) { | |
278 | p.operate(_mem::op::move, &p.buf, &r.buf); | |
279 | r.operate = p.operate; | |
280 | } | |
281 | return r; | |
282 | } | |
283 | ||
11fdf7f2 TL |
284 | template<typename U, std::size_t Z, typename T, std::size_t S> |
285 | static_ptr<U, Z> const_pointer_cast(static_ptr<T, S>&& p) { | |
286 | static_assert(Z >= S, | |
287 | "Value too large."); | |
288 | static_ptr<U, Z> r; | |
289 | if (const_cast<U*>(p.get())) { | |
290 | p.operate(_mem::op::move, &p.buf, &r.buf); | |
291 | r.operate = p.operate; | |
292 | } | |
293 | return r; | |
294 | } | |
295 | ||
296 | // I'm not sure if anyone will ever use this. I can imagine situations | |
297 | // where they might. It works, though! | |
298 | // | |
299 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
11fdf7f2 TL |
300 | static_ptr<U, Z> reinterpret_pointer_cast(static_ptr<T, S>&& p) { |
301 | static_assert(Z >= S, | |
302 | "Value too large."); | |
303 | static_ptr<U, Z> r; | |
304 | p.operate(_mem::op::move, &p.buf, &r.buf); | |
305 | r.operate = p.operate; | |
306 | return r; | |
307 | } | |
308 | ||
309 | // This is the only way to move from a bigger static pointer into a | |
310 | // smaller static pointer. The size of the total data stored in the | |
311 | // pointer is checked at runtime and if the destination size is large | |
312 | // enough, we copy it over. | |
313 | // | |
314 | // I follow cast semantics. Since this is a pointer-like type, it | |
315 | // returns a null value rather than throwing. | |
316 | template<typename U, std::size_t Z, typename T, std::size_t S> | |
11fdf7f2 TL |
317 | static_ptr<U, Z> resize_pointer_cast(static_ptr<T, S>&& p) { |
318 | static_assert(std::is_same_v<U, T>, | |
319 | "resize_pointer_cast only changes size, not type."); | |
320 | static_ptr<U, Z> r; | |
321 | if (Z >= p.operate(_mem::op::size, &p.buf, nullptr)) { | |
322 | p.operate(_mem::op::move, &p.buf, &r.buf); | |
323 | r.operate = p.operate; | |
324 | } | |
325 | return r; | |
326 | } | |
327 | ||
328 | template<typename Base, std::size_t Size> | |
f67539c2 TL |
329 | bool operator ==(const static_ptr<Base, Size>& s, std::nullptr_t) { |
330 | return !s; | |
331 | } | |
332 | template<typename Base, std::size_t Size> | |
333 | bool operator ==(std::nullptr_t, const static_ptr<Base, Size>& s) { | |
334 | return !s; | |
335 | } | |
336 | template<typename Base, std::size_t Size> | |
337 | bool operator ==(static_ptr<Base, Size>& s, std::nullptr_t) { | |
11fdf7f2 TL |
338 | return !s; |
339 | } | |
340 | template<typename Base, std::size_t Size> | |
f67539c2 | 341 | bool operator ==(std::nullptr_t, static_ptr<Base, Size>& s) { |
11fdf7f2 TL |
342 | return !s; |
343 | } | |
344 | ||
345 | // Since `make_unique` and `make_shared` exist, we should follow their | |
346 | // lead. | |
347 | // | |
348 | template<typename Base, typename Derived = Base, | |
349 | std::size_t Size = sizeof(Derived), typename... Args> | |
350 | static_ptr<Base, Size> make_static(Args&& ...args) { | |
351 | return { std::in_place_type<Derived>, std::forward<Args>(args)... }; | |
352 | } | |
353 | } |