diff --git a/xo-alloc2/utest/objectmodel.test.cpp b/xo-alloc2/utest/objectmodel.test.cpp index 8bf1fb67..1f2dc096 100644 --- a/xo-alloc2/utest/objectmodel.test.cpp +++ b/xo-alloc2/utest/objectmodel.test.cpp @@ -2,7 +2,12 @@ * * @author: Roland Conybeare, Dec 2025 * - * Testing rust-like split iface/data object model + * Testing rust-like traits (split iface/data) object model. + * Analogous to: + * - rust traits + * - haskell type classes + * - go interfaces + * * See xo-alloc2/README.md * * Ingredients: @@ -20,9 +25,11 @@ * * Conventions: * 1. abstract interface start with letter A, e.g. AComplex - * 2. representation struct names follow pattern DRepr, e.g. DPolar, DRect. + * 2. representation struct names start with letter D, e.g. DPolar, DRect. * Don't require "intended primary interface" in the name, * since we're seeking ability to attach the same data to different interfaces + * 3. implementations start with letter I. They concatenate abstract interface name + * and representation name, e.g. IComplex_PolarCoords * * Example Class Diagram * @@ -86,6 +93,114 @@ namespace xo { namespace ut { namespace { + struct PlaceholderAbstractInterface { + virtual double foo(void * data) const = 0; + }; + + static_assert(sizeof(PlaceholderAbstractInterface) == sizeof(void*)); + + /** Concept: abstract interface requirements + * Use: when inheriting an abstract interface + * (see also valid_abstract_interface() below) + **/ + template + concept abstract_interface = requires { + std::is_abstract_v, + std::is_polymorphic_v; + /** require no state, just a single vtable pointer **/ + sizeof(T) == sizeof(PlaceholderAbstractInterface); + !std::has_virtual_destructor_v; + std::is_trivially_destructible_v; + }; + + /** For example ISpecific = IComplex_DPolarCoords + **/ + template + concept implements_interface = requires { + std::is_base_of_v; + std::is_default_constructible_v; + std::is_standard_layout_v; + /** require no additional state **/ + sizeof(ISpecific) == sizeof(AInterface); + }; + + /** Router delivers data to interface implementation **/ + template + concept provides_router = requires { + std::is_base_of_v; + sizeof(Router) == sizeof(Object); + }; + + /** Use: when defining an abstract interface AMyInterface + * + * struct AMyInterface { + * virtual void foo(void * data) const = 0; + * }; + * + * static_assert(valid_abstract_interface()); + * + **/ + template + consteval bool valid_abstract_interface() + { + static_assert(std::is_abstract_v, + "Abstract interface expected to have all-abstract methods"); + static_assert(std::is_polymorphic_v, + "Abstract interface expected to have vtable"); + static_assert(sizeof(T) == sizeof(PlaceholderAbstractInterface), + "Abstract interface expected to have no state except for a single vtable pointer"); + static_assert(!std::has_virtual_destructor_v, + "Abstract interface does not benefit from virtual dtor since no state"); + static_assert(std::is_trivially_destructible_v, + "Abstract interface expected to have trivial dtor since no state"); + return true; + }; + + template + consteval bool valid_interface_implementation() + requires (valid_abstract_interface()) + { + static_assert(std::is_base_of_v, + "Interface implementation must inherit abstract interface"); + static_assert(std::is_default_constructible_v, + "Interface implementation must be default-constructible"); + static_assert(sizeof(ISpecific) == sizeof(AInterface), + "Interface implementation may no introduce state"); + static_assert(!std::has_virtual_destructor_v, + "Interface implementation may does not benefit from virtual dtor since no state"); + static_assert(std::is_trivially_destructible_v, + "Interface implementation expected to have trivial dtor since no state"); + + // don't need this test, it's covered by sizeof check + //static_assert(std::is_pointer_interconvertible_base_of_v, + // "Interface implementation must directly inherit interface (no base offset)"); + + return true; + }; + + template + consteval bool valid_object_traits() + { + static_assert(requires { typename OObject::AbstractInterface; }, + "Object type must provide typename Object::AbstractInterface"); + static_assert(requires { typename OObject::ISpecific; }, + "Object type must provide typename Object::ISpecific"); + static_assert(requires { typename OObject::DataType; }, + "Object type must provide typename Object::DataType"); + static_assert(valid_interface_implementation, + "Object::ISpecific must implement Object::AbstractInterface"); + static_assert(std::is_standard_layout_v, + "Object must have standard layout, i.e. no virtual methods. Virtual methods belong in OObject::AbstractInterface"); + static_assert(requires(const OObject & obj) { { obj.iface() } -> std::convertible_to; }, + "Object must have non-virtual method iface() returning const Object::AbstractInterface"); + static_assert(requires(const OObject & obj) { { obj.data() } -> std::convertible_to; }, + "Object must have non-virtual method data() returning Object::DataType*"); + + return true; + } + + // ---------------------------------------------------------------- + /** Associates an interface with an representation. * Specialize to record such associations. **/ @@ -106,37 +221,60 @@ namespace xo { virtual double argument(void * data) const = 0; virtual double magnitude(void * data) const = 0; - virtual void destruct(void * data) const = 0; + virtual void destruct_data(void * data) const = 0; + + private: + static bool _valid; }; + bool + AComplex::_valid = valid_abstract_interface(); + + // ---------------------------------------------------------------- + /** type-erased implementation of AComplex, for runtime polymorphism * Usable by (and only by) overwriting with a typed implementation, * such as IComplex_RectCoords or IComplex_PolarCoords. **/ struct IComplex_Any : public AComplex { - virtual double xcoord(void *) const final override { assert(false); return 0.0; } + virtual double xcoord(void *) const final override { assert(false); return 0.0; } virtual double ycoord(void *) const final override { assert(false); return 0.0; } virtual double argument(void *) const final override { assert(false); return 0.0; } virtual double magnitude(void *) const final override { assert(false); return 0.0; } - virtual void destruct(void *) const final override { assert(false); } + virtual void destruct_data(void *) const final override { assert(false); } + + private: + static bool _valid; }; + bool + IComplex_Any::_valid = valid_interface_implementation; + + // ---------------------------------------------------------------- + template struct IComplex_Specific : public AComplex { static double _xcoord(Repr *); static double _ycoord(Repr *); static double _argument(Repr *); static double _magnitude(Repr *); - static void _destruct(Repr *); + static void _destruct_data(Repr *); virtual double xcoord(void * data) const final override { return _xcoord((Repr*)data); } virtual double ycoord(void * data) const final override { return _ycoord((Repr*)data); } virtual double argument(void * data) const final override { return _argument((Repr*)data); } virtual double magnitude(void * data) const final override { return _magnitude((Repr*)data); } - virtual void destruct(void * data) const final override { _destruct((Repr*)data); } + virtual void destruct_data(void * data) const final override { _destruct_data((Repr*)data); } + + public: + static bool _valid; }; + template + bool + IComplex_Specific::_valid = valid_interface_implementation; + // ----- Placeholder for opaque data ----- // Placeholder used for template specialization @@ -150,7 +288,7 @@ namespace xo { using ImplType = IComplex_Any; }; - // ----- Polar Coordinates ----- + // ----- Representation: Polar Coordinates ----- /** complex number, represented using polar coordinates **/ struct DPolarCoords { @@ -160,6 +298,8 @@ namespace xo { double mag_; }; + // ----- AComplex for DPolarCoords ----- + /** implementation of AComplex interface with representation DPolarCoords **/ using IComplex_DPolarCoords = IComplex_Specific; @@ -188,8 +328,8 @@ namespace xo { template <> void - IComplex_Specific::_destruct(DPolarCoords *) { - /*trivial*/ + IComplex_Specific::_destruct_data(DPolarCoords * data) { + data->~DPolarCoords(); } template <> @@ -197,7 +337,7 @@ namespace xo { using ImplType = IComplex_Specific; }; - // ----- Rectangular Coordinates ----- + // ----- Representation: Rectangular Coordinates ----- /** complex number, represented using rectangular coordinates **/ struct DRectCoords { @@ -207,6 +347,8 @@ namespace xo { double y_; }; + // ----- AComplex for DRectCoords ----- + /** implementation of AComplex interface with representation DRectCoords **/ using IComplex_DRectCoords = IComplex_Specific; @@ -239,8 +381,8 @@ namespace xo { template <> void - IComplex_Specific::_destruct(DRectCoords * /*data*/) { - /*trivial*/ + IComplex_Specific::_destruct_data(DRectCoords * data) { + data->~DRectCoords(); } template <> @@ -248,40 +390,6 @@ namespace xo { using ImplType = IComplex_Specific; }; - // ----- box with unique pointer ----- - - /** - * Creates a 'classic object-oriented' - * instance that has both interface+data. - * - * OUniqueBox uses a unique_ptr to hold data, - * so lifetime ends (unless moved) when this OUniqueBox - * goes out of scope - * - * policy: - * In our object model, these are not intended to be used - * for state; instead create them just-in-time. - * - * - * @tparam ISpecific will be a specific interface, - * such as ISpecificFor - * - * Example: - * OUniqueBox z1 = ..; - * z1._xcoord(z1.data()); - **/ - template - struct OUniqueBox : ISpecificFor::ImplType { - using DataType = Data; - using DataBox = std::unique_ptr; - - explicit OUniqueBox(DataBox d) : data_{std::move(d)} {} - - Data * data() const { return data_.get(); } - - DataBox data_; - }; - // ----- polymorphic box ----- /** @@ -291,46 +399,73 @@ namespace xo { * without additional overhead. Tradeoff is that avoiding such * overhead excludes std::unique_ptr. * - * We're going to instead rely on AInterface providing a destruct() method, + * We're going to instead rely on AInterface providing a destruct_data() method, * so in practice get the deleter from interface state. * * Possibly means we need all abstract interfaces to share a common base + * + * Remarks: + * - when @tparam Data is supplied **/ template - struct OUniqueAny : ISpecificFor::ImplType { + struct OUniqueBox { + using AbstractInterface = AInterface; + using ISpecific = ISpecificFor::ImplType; /* note: Data can be void here */ using DataType = Data; using DataBox = Data*; - explicit OUniqueAny() {} + explicit OUniqueBox() {} /* unsatisfactory b/c doesn't enforce that @p d is heap-allocated */ - explicit OUniqueAny(DataBox d) : data_{std::move(d)} {} + explicit OUniqueBox(DataBox d) : data_{std::move(d)} {} - ~OUniqueAny() { + ~OUniqueBox() { if (data_ != nullptr) { - this->destruct(data_); + this->iface()->destruct_data(data_); delete data_; this->data_ = nullptr; } } - /** note: load-bearing for routing classes such as RComplex **/ + const AInterface * iface() const + requires std::is_same_v + { + return std::launder(&iface_); + } + + const AInterface * iface() const + requires (!std::is_same_v) + { + return &iface_; + } + + /** note: would prefer this to be constexpr, but not simple asof gcc 14.3 **/ + static bool _valid; + + /** note: load-bearing for routing classes such as RComplex **/ Data * data() const { return data_; } + ISpecific iface_; DataBox data_ = nullptr; }; + template + bool + OUniqueBox::_valid = valid_object_traits(); + // ----- Router; RFoo pairs with AFoo ----- + /** For example, inherit OUniqueBox + **/ template struct RComplex : public Object { RComplex() {} RComplex(Object::DataBox data) : Object{std::move(data)} {} - double xcoord() const { return Object::xcoord(Object::data()); } - double ycoord() const { return Object::ycoord(Object::data()); } - double argument() const { return Object::argument(Object::data()); } - double magnitude() const { return Object::magnitude(Object::data()); } + double xcoord() const { return Object::iface()->xcoord(Object::data()); } + double ycoord() const { return Object::iface()->ycoord(Object::data()); } + double argument() const { return Object::iface()->argument(Object::data()); } + double magnitude() const { return Object::iface()->magnitude(Object::data()); } }; template @@ -344,29 +479,14 @@ namespace xo { template using RoutingType = RoutingFor::RoutingType; - // ----- unique box; coordinates with OUniqueBox ----- - - /** boxed object, held by unique pointer - * - * Example: - * ubox z1 = ..; - * z1.xcoord(); - **/ - template - struct ubox : public RoutingType> { - using Super = RoutingType>; - - explicit ubox(Super::DataBox d) : Super{std::move(d)} {} - }; - - // ----- unique any; coordinates with OUniqueAny ----- + // ----- unique any; coordinates with OUniqueBox ----- /** boxed object, held by unique-pointer equiavelent. * * Example: * std::unique_ptr z1_in * = std::make_unique(1.0, 0.0): - * uany z1{z1_in.release()}; + * ubox z1{z1_in.release()}; * z1.xcoord(); * * @@ -381,32 +501,32 @@ namespace xo { * \----->| data :: Repr | * +--------------+ * - * Binary representaiton of unay + * Binary representation of unay * is compatible for different values of @tparam Data * as long as vtable pointer moves along with data pointer. * * In particular binary representation for - * uany is as if it inherited uany + * ubox is as if it inherited ubox * (even though it does not as far as compiler is concerned) * * This is load-bearing for @ref move2any see below **/ template - struct uany : public RoutingType> { - using Super = RoutingType>; + struct ubox : public RoutingType> { + using Super = RoutingType>; - uany() {} - explicit uany(Super::DataBox d) : Super(d) {} + ubox() {} + explicit ubox(Super::DataBox d) : Super(d) {} /** copy contents of this instance into *dest. **/ - void move2any(uany * dest) { - static_assert(sizeof(uany) - == sizeof(uany)); + void move2any(ubox * dest) { + static_assert(sizeof(ubox) + == sizeof(ubox)); - ::memcpy((void*)dest, (void*)this, sizeof(uany)); - // this is almost right. But doesn't copy vtable pointer - //*dest = *(reinterpret_cast*>(this)); + ::memcpy((void*)dest, (void*)this, sizeof(ubox)); + // this is almost right. But would not copy vtable pointer + //*dest = *(reinterpret_cast*>(this)); this->data_ = nullptr; } @@ -416,13 +536,13 @@ namespace xo { * - same strategy (unique / refcounted / ..) **/ template - uany(uany && other) + ubox(ubox && other) requires (std::is_same_v || std::is_convertible_v) : Super() { - static_assert(sizeof(uany) - == sizeof(uany)); + static_assert(sizeof(ubox) + == sizeof(ubox)); other.move2any(this); @@ -460,20 +580,21 @@ namespace xo { TEST_CASE("uniquebox-1", "[objectmodel]") { - OUniqueBox box - {std::make_unique(0.0, 1.0)}; + auto tmp = std::make_unique(0.0, 1.0); + OUniqueBox box{tmp.release()}; - REQUIRE(box.xcoord(box.data()) == 1.0); - REQUIRE(box.ycoord(box.data()) == 0.0); - REQUIRE(box.argument(box.data()) == 0.0); - REQUIRE(box.magnitude(box.data()) == 1.0); + REQUIRE(box.iface()->xcoord(box.data()) == 1.0); + REQUIRE(box.iface()->ycoord(box.data()) == 0.0); + REQUIRE(box.iface()->argument(box.data()) == 0.0); + REQUIRE(box.iface()->magnitude(box.data()) == 1.0); } TEST_CASE("router-1", "[objectmodel]") { using Object = OUniqueBox; + auto tmp = std::make_unique(0.0, 1.0); - RComplex box{std::make_unique(0.0, 1.0)}; + RComplex box{tmp.release()}; REQUIRE(box.xcoord() == 1.0); REQUIRE(box.ycoord() == 0.0); @@ -484,8 +605,9 @@ namespace xo { TEST_CASE("routing-type-1", "[objectmodel]") { using Object = OUniqueBox; + auto tmp = std::make_unique(0.0, 1.0); - RoutingType box{std::make_unique(0.0, 1.0)}; + RoutingType box{tmp.release()}; REQUIRE(box.xcoord() == 1.0); REQUIRE(box.ycoord() == 0.0); @@ -495,7 +617,8 @@ namespace xo { TEST_CASE("ubox-1", "[objectmodel]") { - ubox box{std::make_unique(0.0, 1.0)}; + auto tmp = std::make_unique(0.0, 1.0); + ubox box{tmp.release()}; REQUIRE(box.xcoord() == 1.0); REQUIRE(box.ycoord() == 0.0); @@ -505,7 +628,8 @@ namespace xo { TEST_CASE("ubox-2", "[objectmodel]") { - ubox box{std::make_unique(1.0, 0.0)}; + auto tmp = std::make_unique(1.0, 0.0); + ubox box{tmp.release()}; REQUIRE(box.xcoord() == 1.0); REQUIRE(box.ycoord() == 0.0); @@ -513,16 +637,16 @@ namespace xo { REQUIRE(box.magnitude() == 1.0); } - TEST_CASE("uany-1", "[objectmodel]") + TEST_CASE("ubox-any-1", "[objectmodel]") { /* default ctor */ - uany any; + ubox any; } - TEST_CASE("uany-2", "[objectmodel]") + TEST_CASE("ubox-any-2", "[objectmodel]") { /* equivalent to ubox, but impl doesn't use std::unique_ptr */ - uany any{new DRectCoords{1.0, 0.0}}; + ubox any{new DRectCoords{1.0, 0.0}}; REQUIRE(any.xcoord() == 1.0); REQUIRE(any.ycoord() == 0.0); @@ -530,10 +654,10 @@ namespace xo { REQUIRE(any.magnitude() == 1.0); } - TEST_CASE("uany-3", "[objectmodel]") + TEST_CASE("ubox-any-3", "[objectmodel]") { /* equivalent to ubox, but impl doesn't use std::unique_ptr */ - uany z1{new DRectCoords{1.0, 0.0}}; + ubox z1{new DRectCoords{1.0, 0.0}}; DRectCoords * z1_data = z1.data(); @@ -541,7 +665,7 @@ namespace xo { REQUIRE(z1.xcoord() == 1.0); /* can type-erase */ - uany z1_any; + ubox z1_any; REQUIRE(z1_any.data() == nullptr);