// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- // vim: ts=8 sw=2 smarttab /* * Ceph - scalable distributed file system * * Copyright (C) 2017 Red Hat, Inc. * * This is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License version 2.1, as published by the Free Software * Foundation. See file COPYING. * */ #pragma once #include #include #include namespace ceph { // `static_ptr` // =========== // // It would be really nice if polymorphism didn't require a bunch of // mucking about with the heap. So let's build something where we // don't have to do that. // namespace _mem { // This, an operator function, is one of the canonical ways to do type // erasure in C++ so long as all operations can be done with subsets // of the same arguments (which is not true for function type erasure) // it's a pretty good one. enum class op { move, destroy, size }; template static std::size_t op_fun(op oper, void* p1, void* p2) { auto me = static_cast(p1); switch (oper) { case op::move: new (p2) T(std::move(*me)); break; case op::destroy: me->~T(); break; case op::size: return sizeof(T); } return 0; } } // The thing itself! // // The default value for Size may be wrong in almost all cases. You // can change it to your heart's content. The upside is that you'll // just get a compile error and you can bump it up. // // I *recommend* having a size constant in header files (or perhaps a // using declaration, e.g. // ``` // using StaticFoo = static_ptr` // ``` // in some header file that can be used multiple places) so that when // you create a new derived class with a larger size, you only have to // change it in one place. // template class static_ptr { template friend class static_ptr; // Refuse to be set to anything with whose type we are // incompatible. Also never try to eat anything bigger than you are. // template constexpr static int create_ward() noexcept { static_assert(std::is_void_v || std::is_base_of_v>, "Value to store must be a derivative of the base."); static_assert(S <= Size, "Value too large."); static_assert(std::is_void_v || !std::is_const{} || std::is_const_v, "Cannot assign const pointer to non-const pointer."); return 0; } // Here we can store anything that has the same signature, which is // relevant to the multiple-versions for move/copy support that I // mentioned above. // size_t (*operate)(_mem::op, void*, void*); // This is mutable so that get and the dereference operators can be // const. Since we're modeling a pointer, we should preserve the // difference in semantics between a pointer-to-const and a const // pointer. // mutable typename std::aligned_storage::type buf; public: using element_type = Base; using pointer = Base*; // Empty static_ptr() noexcept : operate(nullptr) {} static_ptr(std::nullptr_t) noexcept : operate(nullptr) {} static_ptr& operator =(std::nullptr_t) noexcept { reset(); return *this; } ~static_ptr() noexcept { reset(); } // Since other pointer-ish types have it void reset() noexcept { if (operate) { operate(_mem::op::destroy, &buf, nullptr); operate = nullptr; } } // Set from another static pointer. // // Since the templated versions don't count for overriding the defaults static_ptr(static_ptr&& rhs) noexcept(std::is_nothrow_move_constructible_v) : operate(rhs.operate) { if (operate) { operate(_mem::op::move, &rhs.buf, &buf); } } template static_ptr(static_ptr&& rhs) noexcept(std::is_nothrow_move_constructible_v) : operate(rhs.operate) { create_ward(); if (operate) { operate(_mem::op::move, &rhs.buf, &buf); } } static_ptr& operator =(static_ptr&& rhs) noexcept(std::is_nothrow_move_constructible_v) { reset(); if (rhs) { operate = rhs.operate; operate(_mem::op::move, &rhs.buf, &buf); } return *this; } template static_ptr& operator =(static_ptr&& rhs) noexcept(std::is_nothrow_move_constructible_v) { create_ward(); reset(); if (rhs) { operate = rhs.operate; operate(_mem::op::move, &rhs.buf, &buf); } return *this; } // In-place construction! // // This is basically what you want, and I didn't include value // construction because in-place construction renders it // unnecessary. Also it doesn't fit the pointer idiom as well. // template static_ptr(std::in_place_type_t, Args&& ...args) noexcept(std::is_nothrow_constructible_v) : operate(&_mem::op_fun){ static_assert((!std::is_nothrow_copy_constructible_v || std::is_nothrow_copy_constructible_v) && (!std::is_nothrow_move_constructible_v || std::is_nothrow_move_constructible_v), "If declared type of static_ptr is nothrow " "move/copy constructible, then any " "type assigned to it must be as well. " "You can use reinterpret_pointer_cast " "to get around this limit, but don't " "come crying to me when the C++ " "runtime calls terminate()."); create_ward(); new (&buf) T(std::forward(args)...); } // I occasionally get tempted to make an overload of the assignment // operator that takes a tuple as its right-hand side to provide // arguments. // template void emplace(Args&& ...args) noexcept(std::is_nothrow_constructible_v) { create_ward(); reset(); operate = &_mem::op_fun; new (&buf) T(std::forward(args)...); } // Access! Base* get() const noexcept { return operate ? reinterpret_cast(&buf) : nullptr; } template std::enable_if_t, Base*> operator->() const noexcept { return get(); } template std::enable_if_t, Base&> operator *() const noexcept { return *get(); } operator bool() const noexcept { return !!operate; } // Big wall of friendship // template friend static_ptr static_pointer_cast(const static_ptr& p); template friend static_ptr static_pointer_cast(static_ptr&& p); template friend static_ptr dynamic_pointer_cast(const static_ptr& p); template friend static_ptr dynamic_pointer_cast(static_ptr&& p); template friend static_ptr const_pointer_cast(const static_ptr& p); template friend static_ptr const_pointer_cast(static_ptr&& p); template friend static_ptr reinterpret_pointer_cast(const static_ptr& p); template friend static_ptr reinterpret_pointer_cast(static_ptr&& p); template friend static_ptr resize_pointer_cast(const static_ptr& p); template friend static_ptr resize_pointer_cast(static_ptr&& p); }; // These are all modeled after the same ones for shared pointer. // // Also I'm annoyed that the standard library doesn't have // *_pointer_cast overloads for a move-only unique pointer. It's a // nice idiom. Having to release and reconstruct is obnoxious. // template static_ptr static_pointer_cast(static_ptr&& p) { static_assert(Z >= S, "Value too large."); static_ptr r; if (static_cast(p.get())) { p.operate(_mem::op::move, &p.buf, &r.buf); r.operate = p.operate; } return r; } // Here the conditional is actually important and ensures we have the // same behavior as dynamic_cast. // template static_ptr dynamic_pointer_cast(static_ptr&& p) { static_assert(Z >= S, "Value too large."); static_ptr r; if (dynamic_cast(p.get())) { p.operate(_mem::op::move, &p.buf, &r.buf); r.operate = p.operate; } return r; } template static_ptr const_pointer_cast(static_ptr&& p) { static_assert(Z >= S, "Value too large."); static_ptr r; if (const_cast(p.get())) { p.operate(_mem::op::move, &p.buf, &r.buf); r.operate = p.operate; } return r; } // I'm not sure if anyone will ever use this. I can imagine situations // where they might. It works, though! // template static_ptr reinterpret_pointer_cast(static_ptr&& p) { static_assert(Z >= S, "Value too large."); static_ptr r; p.operate(_mem::op::move, &p.buf, &r.buf); r.operate = p.operate; return r; } // This is the only way to move from a bigger static pointer into a // smaller static pointer. The size of the total data stored in the // pointer is checked at runtime and if the destination size is large // enough, we copy it over. // // I follow cast semantics. Since this is a pointer-like type, it // returns a null value rather than throwing. template static_ptr resize_pointer_cast(static_ptr&& p) { static_assert(std::is_same_v, "resize_pointer_cast only changes size, not type."); static_ptr r; if (Z >= p.operate(_mem::op::size, &p.buf, nullptr)) { p.operate(_mem::op::move, &p.buf, &r.buf); r.operate = p.operate; } return r; } template bool operator ==(const static_ptr& s, std::nullptr_t) { return !s; } template bool operator ==(std::nullptr_t, const static_ptr& s) { return !s; } template bool operator ==(static_ptr& s, std::nullptr_t) { return !s; } template bool operator ==(std::nullptr_t, static_ptr& s) { return !s; } // Since `make_unique` and `make_shared` exist, we should follow their // lead. // template static_ptr make_static(Args&& ...args) { return { std::in_place_type, std::forward(args)... }; } }