1 // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
2 // vim: ts=8 sw=2 smarttab
4 * Ceph - scalable distributed file system
6 * Copyright (C) 2017 Red Hat, Inc.
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.
19 #include <type_traits>
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.
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.
36 copy
, move
, destroy
, size
39 static std::size_t op_fun(op oper
, void* p1
, void* p2
)
41 auto me
= static_cast<T
*>(p1
);
45 // One conspicuous downside is that immovable/uncopyable functions
46 // kill compilation right here, even if nobody ever calls the move
47 // or copy methods. Working around this is a pain, since we'd need
48 // four operator functions and a top-level class to
49 // provide/withhold copy/move operations as appropriate.
54 new (p2
) T(std::move(*me
));
69 // The default value for Size may be wrong in almost all cases. You
70 // can change it to your heart's content. The upside is that you'll
71 // just get a compile error and you can bump it up.
73 // I *recommend* having a size constant in header files (or perhaps a
74 // using declaration, e.g.
76 // using StaticFoo = static_ptr<Foo, sizeof(Blah)>`
78 // in some header file that can be used multiple places) so that when
79 // you create a new derived class with a larger size, you only have to
80 // change it in one place.
82 template<typename Base
, std::size_t Size
= sizeof(Base
)>
84 template<typename U
, std::size_t S
>
85 friend class static_ptr
;
87 // Refuse to be set to anything with whose type we are
88 // incompatible. Also never try to eat anything bigger than you are.
90 template<typename T
, std::size_t S
>
91 constexpr static int create_ward() noexcept
{
92 static_assert(std::is_void_v
<Base
> ||
93 std::is_base_of_v
<Base
, std::decay_t
<T
>>,
94 "Value to store must be a derivative of the base.");
95 static_assert(S
<= Size
, "Value too large.");
96 static_assert(std::is_void_v
<Base
> || !std::is_const
<Base
>{} ||
98 "Cannot assign const pointer to non-const pointer.");
101 // Here we can store anything that has the same signature, which is
102 // relevant to the multiple-versions for move/copy support that I
105 size_t (*operate
)(_mem::op
, void*, void*);
107 // This is mutable so that get and the dereference operators can be
108 // const. Since we're modeling a pointer, we should preserve the
109 // difference in semantics between a pointer-to-const and a const
112 mutable typename
std::aligned_storage
<Size
>::type buf
;
115 using element_type
= Base
;
116 using pointer
= Base
*;
119 static_ptr() noexcept
: operate(nullptr) {}
120 static_ptr(std::nullptr_t
) noexcept
: operate(nullptr) {}
121 static_ptr
& operator =(std::nullptr_t
) noexcept
{
125 ~static_ptr() noexcept
{
129 // Since other pointer-ish types have it
130 void reset() noexcept
{
132 operate(_mem::op::destroy
, &buf
, nullptr);
137 // Set from another static pointer.
139 // Since the templated versions don't count for overriding the defaults
140 static_ptr(const static_ptr
& rhs
)
141 noexcept(std::is_nothrow_copy_constructible_v
<Base
>) : operate(rhs
.operate
) {
143 operate(_mem::op::copy
, &rhs
.buf
, &buf
);
146 static_ptr(static_ptr
&& rhs
)
147 noexcept(std::is_nothrow_move_constructible_v
<Base
>) : operate(rhs
.operate
) {
149 operate(_mem::op::move
, &rhs
.buf
, &buf
);
153 template<typename U
, std::size_t S
>
154 static_ptr(const static_ptr
<U
, S
>& rhs
)
155 noexcept(std::is_nothrow_copy_constructible_v
<U
>) : operate(rhs
.operate
) {
158 operate(_mem::op::copy
, &rhs
.buf
, &buf
);
161 template<typename U
, std::size_t S
>
162 static_ptr(static_ptr
<U
, S
>&& rhs
)
163 noexcept(std::is_nothrow_move_constructible_v
<U
>) : operate(rhs
.operate
) {
166 operate(_mem::op::move
, &rhs
.buf
, &buf
);
170 static_ptr
& operator =(const static_ptr
& rhs
)
171 noexcept(std::is_nothrow_copy_constructible_v
<Base
>) {
174 operate
= rhs
.operate
;
175 operate(_mem::op::copy
,
176 const_cast<void*>(static_cast<const void*>(&rhs
.buf
)), &buf
);
180 static_ptr
& operator =(static_ptr
&& rhs
)
181 noexcept(std::is_nothrow_move_constructible_v
<Base
>) {
184 operate
= rhs
.operate
;
185 operate(_mem::op::move
, &rhs
.buf
, &buf
);
190 template<typename U
, std::size_t S
>
191 static_ptr
& operator =(const static_ptr
<U
, S
>& rhs
)
192 noexcept(std::is_nothrow_copy_constructible_v
<U
>) {
196 operate
= rhs
.operate
;
197 operate(_mem::op::copy
,
198 const_cast<void*>(static_cast<const void*>(&rhs
.buf
)), &buf
);
202 template<typename U
, std::size_t S
>
203 static_ptr
& operator =(static_ptr
<U
, S
>&& rhs
)
204 noexcept(std::is_nothrow_move_constructible_v
<U
>) {
208 operate
= rhs
.operate
;
209 operate(_mem::op::move
, &rhs
.buf
, &buf
);
214 // In-place construction!
216 // This is basically what you want, and I didn't include value
217 // construction because in-place construction renders it
218 // unnecessary. Also it doesn't fit the pointer idiom as well.
220 template<typename T
, typename
... Args
>
221 static_ptr(std::in_place_type_t
<T
>, Args
&& ...args
)
222 noexcept(std::is_nothrow_constructible_v
<T
, Args
...>)
223 : operate(&_mem::op_fun
<T
>){
224 static_assert((!std::is_nothrow_copy_constructible_v
<Base
> ||
225 std::is_nothrow_copy_constructible_v
<T
>) &&
226 (!std::is_nothrow_move_constructible_v
<Base
> ||
227 std::is_nothrow_move_constructible_v
<T
>),
228 "If declared type of static_ptr is nothrow "
229 "move/copy constructible, then any "
230 "type assigned to it must be as well. "
231 "You can use reinterpret_pointer_cast "
232 "to get around this limit, but don't "
233 "come crying to me when the C++ "
234 "runtime calls terminate().");
235 create_ward
<T
, sizeof(T
)>();
236 new (&buf
) T(std::forward
<Args
>(args
)...);
239 // I occasionally get tempted to make an overload of the assignment
240 // operator that takes a tuple as its right-hand side to provide
243 template<typename T
, typename
... Args
>
244 void emplace(Args
&& ...args
)
245 noexcept(std::is_nothrow_constructible_v
<T
, Args
...>) {
246 create_ward
<T
, sizeof(T
)>();
248 operate
= &_mem::op_fun
<T
>;
249 new (&buf
) T(std::forward
<Args
>(args
)...);
253 Base
* get() const noexcept
{
254 return operate
? reinterpret_cast<Base
*>(&buf
) : nullptr;
256 template<typename U
= Base
>
257 std::enable_if_t
<!std::is_void_v
<U
>, Base
*> operator->() const noexcept
{
260 template<typename U
= Base
>
261 std::enable_if_t
<!std::is_void_v
<U
>, Base
&> operator *() const noexcept
{
264 operator bool() const noexcept
{
268 // Big wall of friendship
270 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
271 friend static_ptr
<U
, Z
> static_pointer_cast(const static_ptr
<T
, S
>& p
);
272 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
273 friend static_ptr
<U
, Z
> static_pointer_cast(static_ptr
<T
, S
>&& p
);
275 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
276 friend static_ptr
<U
, Z
> dynamic_pointer_cast(const static_ptr
<T
, S
>& p
);
277 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
278 friend static_ptr
<U
, Z
> dynamic_pointer_cast(static_ptr
<T
, S
>&& p
);
280 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
281 friend static_ptr
<U
, Z
> const_pointer_cast(const static_ptr
<T
, S
>& p
);
282 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
283 friend static_ptr
<U
, Z
> const_pointer_cast(static_ptr
<T
, S
>&& p
);
285 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
286 friend static_ptr
<U
, Z
> reinterpret_pointer_cast(const static_ptr
<T
, S
>& p
);
287 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
288 friend static_ptr
<U
, Z
> reinterpret_pointer_cast(static_ptr
<T
, S
>&& p
);
290 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
291 friend static_ptr
<U
, Z
> resize_pointer_cast(const static_ptr
<T
, S
>& p
);
292 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
293 friend static_ptr
<U
, Z
> resize_pointer_cast(static_ptr
<T
, S
>&& p
);
296 // These are all modeled after the same ones for shared pointer.
298 // Also I'm annoyed that the standard library doesn't have
299 // *_pointer_cast overloads for a move-only unique pointer. It's a
300 // nice idiom. Having to release and reconstruct is obnoxious.
302 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
303 static_ptr
<U
, Z
> static_pointer_cast(const static_ptr
<T
, S
>& p
) {
304 static_assert(Z
>= S
,
307 // Really, this is always true because static_cast either succeeds
308 // or fails to compile, but it prevents an unused variable warning
309 // and should be optimized out.
310 if (static_cast<U
*>(p
.get())) {
311 p
.operate(_mem::op::copy
, &p
.buf
, &r
.buf
);
312 r
.operate
= p
.operate
;
316 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
317 static_ptr
<U
, Z
> static_pointer_cast(static_ptr
<T
, S
>&& p
) {
318 static_assert(Z
>= S
,
321 if (static_cast<U
*>(p
.get())) {
322 p
.operate(_mem::op::move
, &p
.buf
, &r
.buf
);
323 r
.operate
= p
.operate
;
328 // Here the conditional is actually important and ensures we have the
329 // same behavior as dynamic_cast.
331 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
332 static_ptr
<U
, Z
> dynamic_pointer_cast(const static_ptr
<T
, S
>& p
) {
333 static_assert(Z
>= S
,
336 if (dynamic_cast<U
*>(p
.get())) {
337 p
.operate(_mem::op::copy
, &p
.buf
, &r
.buf
);
338 r
.operate
= p
.operate
;
342 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
343 static_ptr
<U
, Z
> dynamic_pointer_cast(static_ptr
<T
, S
>&& p
) {
344 static_assert(Z
>= S
,
347 if (dynamic_cast<U
*>(p
.get())) {
348 p
.operate(_mem::op::move
, &p
.buf
, &r
.buf
);
349 r
.operate
= p
.operate
;
354 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
355 static_ptr
<U
, Z
> const_pointer_cast(const static_ptr
<T
, S
>& p
) {
356 static_assert(Z
>= S
,
359 if (const_cast<U
*>(p
.get())) {
360 p
.operate(_mem::op::copy
, &p
.buf
, &r
.buf
);
361 r
.operate
= p
.operate
;
365 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
366 static_ptr
<U
, Z
> const_pointer_cast(static_ptr
<T
, S
>&& p
) {
367 static_assert(Z
>= S
,
370 if (const_cast<U
*>(p
.get())) {
371 p
.operate(_mem::op::move
, &p
.buf
, &r
.buf
);
372 r
.operate
= p
.operate
;
377 // I'm not sure if anyone will ever use this. I can imagine situations
378 // where they might. It works, though!
380 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
381 static_ptr
<U
, Z
> reinterpret_pointer_cast(const static_ptr
<T
, S
>& p
) {
382 static_assert(Z
>= S
,
385 p
.operate(_mem::op::copy
, &p
.buf
, &r
.buf
);
386 r
.operate
= p
.operate
;
389 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
390 static_ptr
<U
, Z
> reinterpret_pointer_cast(static_ptr
<T
, S
>&& p
) {
391 static_assert(Z
>= S
,
394 p
.operate(_mem::op::move
, &p
.buf
, &r
.buf
);
395 r
.operate
= p
.operate
;
399 // This is the only way to move from a bigger static pointer into a
400 // smaller static pointer. The size of the total data stored in the
401 // pointer is checked at runtime and if the destination size is large
402 // enough, we copy it over.
404 // I follow cast semantics. Since this is a pointer-like type, it
405 // returns a null value rather than throwing.
406 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
407 static_ptr
<U
, Z
> resize_pointer_cast(const static_ptr
<T
, S
>& p
) {
408 static_assert(std::is_same_v
<U
, T
>,
409 "resize_pointer_cast only changes size, not type.");
411 if (Z
>= p
.operate(_mem::op::size
, &p
.buf
, nullptr)) {
412 p
.operate(_mem::op::copy
, &p
.buf
, &r
.buf
);
413 r
.operate
= p
.operate
;
417 template<typename U
, std::size_t Z
, typename T
, std::size_t S
>
418 static_ptr
<U
, Z
> resize_pointer_cast(static_ptr
<T
, S
>&& p
) {
419 static_assert(std::is_same_v
<U
, T
>,
420 "resize_pointer_cast only changes size, not type.");
422 if (Z
>= p
.operate(_mem::op::size
, &p
.buf
, nullptr)) {
423 p
.operate(_mem::op::move
, &p
.buf
, &r
.buf
);
424 r
.operate
= p
.operate
;
429 template<typename Base
, std::size_t Size
>
430 bool operator ==(static_ptr
<Base
, Size
> s
, std::nullptr_t
) {
433 template<typename Base
, std::size_t Size
>
434 bool operator ==(std::nullptr_t
, static_ptr
<Base
, Size
> s
) {
438 // Since `make_unique` and `make_shared` exist, we should follow their
441 template<typename Base
, typename Derived
= Base
,
442 std::size_t Size
= sizeof(Derived
), typename
... Args
>
443 static_ptr
<Base
, Size
> make_static(Args
&& ...args
) {
444 return { std::in_place_type
<Derived
>, std::forward
<Args
>(args
)... };