From 11f54d730d3574b795aa86cfca382a2a1ae2b1b8 Mon Sep 17 00:00:00 2001 From: "Adam C. Emerson" Date: Tue, 21 Nov 2017 14:13:48 -0500 Subject: [PATCH] common: Add static_ptr Add static_ptr, a pointer-like class that contains its own storage, avoiding use of the heap. The full range of *_pointer_cast functions are included. Signed-off-by: Adam C. Emerson --- src/common/static_ptr.h | 442 +++++++++++++++++++++++++++++ src/test/common/CMakeLists.txt | 3 + src/test/common/test_static_ptr.cc | 260 +++++++++++++++++ 3 files changed, 705 insertions(+) create mode 100644 src/common/static_ptr.h create mode 100644 src/test/common/test_static_ptr.cc diff --git a/src/common/static_ptr.h b/src/common/static_ptr.h new file mode 100644 index 0000000000000..9e4ea101aa963 --- /dev/null +++ b/src/common/static_ptr.h @@ -0,0 +1,442 @@ +// -*- 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. + * + */ + +#include "common/backport14.h" + +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 { + copy, 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::copy: + // One conspicuous downside is that immovable/uncopyable functions + // kill compilation right here, even if nobody ever calls the move + // or copy methods. Working around this is a pain, since we'd need + // four operator functions and a top-level class to + // provide/withhold copy/move operations as appropriate. + new (p2) T(*me); + break; + + 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{} || + std::is_base_of>{}, + "Value to store must be a derivative of the base."); + static_assert(S <= Size, "Value too large."); + static_assert(std::is_void{} || !std::is_const{} || + std::is_const{}, + "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(const static_ptr& rhs) + noexcept(std::is_nothrow_copy_constructible{}) : operate(rhs.operate) { + if (operate) { + operate(_mem::op::copy, &rhs.buf, &buf); + } + } + static_ptr(static_ptr&& rhs) + noexcept(std::is_nothrow_move_constructible{}) : operate(rhs.operate) { + if (operate) { + operate(_mem::op::move, &rhs.buf, &buf); + } + } + + template + static_ptr(const static_ptr& rhs) + noexcept(std::is_nothrow_copy_constructible{}) : operate(rhs.operate) { + create_ward(); + if (operate) { + operate(_mem::op::copy, &rhs.buf, &buf); + } + } + template + static_ptr(static_ptr&& rhs) + noexcept(std::is_nothrow_move_constructible{}) : operate(rhs.operate) { + create_ward(); + if (operate) { + operate(_mem::op::move, &rhs.buf, &buf); + } + } + + static_ptr& operator =(const static_ptr& rhs) + noexcept(std::is_nothrow_copy_constructible{}) { + reset(); + if (rhs) { + operate = rhs.operate; + operate(_mem::op::copy, + const_cast(static_cast(&rhs.buf)), &buf); + } + return *this; + } + static_ptr& operator =(static_ptr&& rhs) + noexcept(std::is_nothrow_move_constructible{}) { + reset(); + if (rhs) { + operate = rhs.operate; + operate(_mem::op::move, &rhs.buf, &buf); + } + return *this; + } + + template + static_ptr& operator =(const static_ptr& rhs) + noexcept(std::is_nothrow_copy_constructible{}) { + create_ward(); + reset(); + if (rhs) { + operate = rhs.operate; + operate(_mem::op::copy, + const_cast(static_cast(&rhs.buf)), &buf); + } + return *this; + } + template + static_ptr& operator =(static_ptr&& rhs) + noexcept(std::is_nothrow_move_constructible{}) { + 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(in_place_type_t, Args&& ...args) + noexcept(std::is_nothrow_constructible{}) + : operate(&_mem::op_fun){ + static_assert((!std::is_nothrow_copy_constructible{} || + std::is_nothrow_copy_constructible{}) && + (!std::is_nothrow_move_constructible{} || + std::is_nothrow_move_constructible{}), + "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{}) { + 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 + enable_if_t{}, Base*> operator->() const noexcept { + return get(); + } + template + 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(const static_ptr& p) { + static_assert(Z >= S, + "Value too large."); + static_ptr r; + // Really, this is always true because static_cast either succeeds + // or fails to compile, but it prevents an unused variable warning + // and should be optimized out. + if (static_cast(p.get())) { + p.operate(_mem::op::copy, &p.buf, &r.buf); + r.operate = p.operate; + } + return r; +} +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(const static_ptr& p) { + static_assert(Z >= S, + "Value too large."); + static_ptr r; + if (dynamic_cast(p.get())) { + p.operate(_mem::op::copy, &p.buf, &r.buf); + r.operate = p.operate; + } + return r; +} +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(const static_ptr& p) { + static_assert(Z >= S, + "Value too large."); + static_ptr r; + if (const_cast(p.get())) { + p.operate(_mem::op::copy, &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(const static_ptr& p) { + static_assert(Z >= S, + "Value too large."); + static_ptr r; + p.operate(_mem::op::copy, &p.buf, &r.buf); + r.operate = p.operate; + return r; +} +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(const static_ptr& p) { + static_assert(std::is_same{}, + "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::copy, &p.buf, &r.buf); + r.operate = p.operate; + } + return r; +} +template +static_ptr resize_pointer_cast(static_ptr&& p) { + static_assert(std::is_same{}, + "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 ==(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 { in_place_type_t{}, std::forward(args)... }; +} +} diff --git a/src/test/common/CMakeLists.txt b/src/test/common/CMakeLists.txt index 7be766079d16a..bb5d892da876a 100644 --- a/src/test/common/CMakeLists.txt +++ b/src/test/common/CMakeLists.txt @@ -277,3 +277,6 @@ add_executable(unittest_bounded_key_counter $) target_link_libraries(unittest_bounded_key_counter global) add_ceph_unittest(unittest_bounded_key_counter) + +add_executable(unittest_static_ptr test_static_ptr.cc) +add_ceph_unittest(unittest_static_ptr) diff --git a/src/test/common/test_static_ptr.cc b/src/test/common/test_static_ptr.cc new file mode 100644 index 0000000000000..4a464d79f548d --- /dev/null +++ b/src/test/common/test_static_ptr.cc @@ -0,0 +1,260 @@ +// -*- 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. + * + */ + +#include "common/static_ptr.h" +#include + +using ceph::static_ptr; +using ceph::make_static; + +class base { +public: + virtual int func() = 0; + virtual ~base() = default; +}; + +class sibling1 : public base { +public: + int func() override { return 0; } +}; + +class sibling2 : public base { +public: + int func() override { return 9; } + virtual int call(int) = 0; +}; + +class grandchild : public sibling2 { +protected: + int val; +public: + explicit grandchild(int val) : val(val) {} + virtual int call(int n) { return n * val; } +}; + +class great_grandchild : public grandchild { +public: + great_grandchild(int val) : grandchild(val) {} + int call(int n) override { return n + val; } +}; + +TEST(StaticPtr, EmptyCreation) { + static_ptr p; + EXPECT_FALSE(p); + EXPECT_EQ(p, nullptr); + EXPECT_EQ(nullptr, p); + EXPECT_TRUE(p.get() == nullptr); +} + +TEST(StaticPtr, CreationCall) { + { + static_ptr p(ceph::in_place_type_t{}); + EXPECT_TRUE(p); + EXPECT_FALSE(p == nullptr); + EXPECT_FALSE(nullptr == p); + EXPECT_FALSE(p.get() == nullptr); + EXPECT_EQ(p->func(), 0); + EXPECT_EQ((*p).func(), 0); + EXPECT_EQ((p.get())->func(), 0); + } + { + auto p = make_static(); + EXPECT_TRUE(p); + EXPECT_FALSE(p == nullptr); + EXPECT_FALSE(nullptr == p); + EXPECT_FALSE(p.get() == nullptr); + EXPECT_EQ(p->func(), 0); + EXPECT_EQ((*p).func(), 0); + EXPECT_EQ((p.get())->func(), 0); + } +} + +TEST(StaticPtr, CreateReset) { + { + static_ptr p(ceph::in_place_type_t{}); + EXPECT_EQ((p.get())->func(), 0); + p.reset(); + EXPECT_FALSE(p); + EXPECT_EQ(p, nullptr); + EXPECT_EQ(nullptr, p); + EXPECT_TRUE(p.get() == nullptr); + } + { + static_ptr p(ceph::in_place_type_t{}); + EXPECT_EQ((p.get())->func(), 0); + p = nullptr; + EXPECT_FALSE(p); + EXPECT_EQ(p, nullptr); + EXPECT_EQ(nullptr, p); + EXPECT_TRUE(p.get() == nullptr); + } +} + +TEST(StaticPtr, CreateEmplace) { + static_ptr p(ceph::in_place_type_t{}); + EXPECT_EQ((p.get())->func(), 0); + p.emplace(30); + EXPECT_EQ(p->func(), 9); +} + +TEST(StaticPtr, CopyMove) { + // Won't compile. Good. + // static_ptr p1(ceph::in_place_type_t{}, 3); + + static_ptr p1(ceph::in_place_type_t{}); + static_ptr p2(ceph::in_place_type_t{}, + 3); + + // This also does not compile. Good. + // p1 = p2; + p2 = p1; + EXPECT_EQ(p1->func(), 0); + + p2 = std::move(p1); + EXPECT_EQ(p1->func(), 0); +} + +TEST(StaticPtr, ImplicitUpcast) { + static_ptr p1; + static_ptr p2(ceph::in_place_type_t{}, 3); + + p1 = p2; + EXPECT_EQ(p1->func(), 9); + + p1 = std::move(p2); + EXPECT_EQ(p1->func(), 9); + + p2.reset(); + + // Doesn't compile. Good. + // p2 = p1; +} + +TEST(StaticPtr, StaticCast) { + static_ptr p1(ceph::in_place_type_t{}, 3); + static_ptr p2; + + p2 = ceph::static_pointer_cast(p1); + EXPECT_EQ(p2->func(), 9); + EXPECT_EQ(p2->call(10), 30); + + p2 = ceph::static_pointer_cast(std::move(p1)); + EXPECT_EQ(p2->func(), 9); + EXPECT_EQ(p2->call(10), 30); +} + +TEST(StaticPtr, DynamicCast) { + static constexpr auto sz = sizeof(great_grandchild); + { + static_ptr p1(ceph::in_place_type_t{}, 3); + auto p2 = ceph::dynamic_pointer_cast(p1); + EXPECT_FALSE(p2); + } + { + static_ptr p1(ceph::in_place_type_t{}, 3); + auto p2 = ceph::dynamic_pointer_cast(std::move(p1)); + EXPECT_FALSE(p2); + } + + { + static_ptr p1(ceph::in_place_type_t{}, 3); + auto p2 = ceph::dynamic_pointer_cast(p1); + EXPECT_TRUE(p2); + EXPECT_EQ(p2->func(), 9); + EXPECT_EQ(p2->call(10), 30); + } + { + static_ptr p1(ceph::in_place_type_t{}, 3); + auto p2 = ceph::dynamic_pointer_cast(std::move(p1)); + EXPECT_TRUE(p2); + EXPECT_EQ(p2->func(), 9); + EXPECT_EQ(p2->call(10), 30); + } +} + +class constable { +public: + int foo() { + return 2; + } + int foo() const { + return 5; + } +}; + +TEST(StaticPtr, ConstCast) { + static constexpr auto sz = sizeof(constable); + { + auto p1 = make_static(); + static_assert(std::is_const{}, + "Things are not as const as they ought to be."); + EXPECT_EQ(p1->foo(), 5); + auto p2 = ceph::const_pointer_cast(p1); + static_assert(!std::is_const{}, + "Things are more const than they ought to be."); + EXPECT_TRUE(p2); + EXPECT_EQ(p2->foo(), 2); + } + { + auto p1 = make_static(); + EXPECT_EQ(p1->foo(), 5); + auto p2 = ceph::const_pointer_cast(std::move(p1)); + static_assert(!std::is_const{}, + "Things are more const than they ought to be."); + EXPECT_TRUE(p2); + EXPECT_EQ(p2->foo(), 2); + } +} + +TEST(StaticPtr, ReinterpretCast) { + static constexpr auto sz = sizeof(grandchild); + { + auto p1 = make_static(3); + auto p2 = ceph::reinterpret_pointer_cast(p1); + static_assert(std::is_same{}, + "Reinterpret is screwy."); + auto p3 = ceph::reinterpret_pointer_cast(p2); + static_assert(std::is_same{}, + "Reinterpret is screwy."); + EXPECT_EQ(p3->func(), 9); + EXPECT_EQ(p3->call(10), 30); + } + { + auto p1 = make_static(3); + auto p2 = ceph::reinterpret_pointer_cast(std::move(p1)); + static_assert(std::is_same{}, + "Reinterpret is screwy."); + auto p3 = ceph::reinterpret_pointer_cast(std::move(p2)); + static_assert(std::is_same{}, + "Reinterpret is screwy."); + EXPECT_EQ(p3->func(), 9); + EXPECT_EQ(p3->call(10), 30); + } +} + +struct exceptional { + exceptional() = default; + exceptional(const exceptional& e) { + throw std::exception(); + } + exceptional(exceptional&& e) { + throw std::exception(); + } +}; + +TEST(StaticPtr, Exceptional) { + static_ptr p1(ceph::in_place_type_t{}); + EXPECT_ANY_THROW(static_ptr p2(p1)); + EXPECT_ANY_THROW(static_ptr p2(std::move(p1))); +} -- 2.39.5