/** @file objectmodel.test.cpp * * @author: Roland Conybeare, Dec 2025 * * Testing rust-like traits (split iface/data) object model. * Analogous to: * - rust traits * - haskell type classes * - go interfaces * * See xo-alloc2/README.md * * Ingredients: * 1. abstract interface: all virtual methods. No assumptions about representation. * No state (besides implict vtable pointer) * * Rules: * 1. abstract interface must have no state besides implicit vtable pointer. * This is a strongly-held principle, we're keeping data representation entirely * separate * 2. representations as passive as possible. No getters. All public members. * Exceptions to this principle: * - ctors (including copy/move ctors, when needed) * - dtors * * Conventions: * 1. abstract interface start with letter A, e.g. AComplex * 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 * * AComplex * ^ * | * /------------------------+--------------------\ * | | | * IComplex_DRectCoords IComplex_DPolarCoords IComplex_Any * = IComplex_Specific = IComplex_Specific * * ^ ^ * | | * ... OUniqueBox * * ^ * | * RComplex * = RoutingFor::RoutingType * ^ * | * ubox * * AComplex: abstract interface. * explicit, type-erased, data pointer argument * virtual AComplex::xcoord(void * data) * * DPolarCoords: passive representation * * IComplex_DPolarCoords: implement AComplex interface for representation DPolarCoords * static methods with typed data pointer argument * IComplex_DPolarCoords::xcoord(void * data) * IComplex_DPolarCoords::_xcoord(DPolarCoords * data) * * OUniqueBox: * a self-sufficient object, associating * interface AComplex with representation DPolarCoords * OUniqueBox .data() method is DPolarCoord* * 'impure' in the sense that it mixes code+data * * RComplex: convenience interface for OUniqueBox * * ubox: * self-sufficent object with convenient interface * * Application code will deal with ubox **/ #include #include #include #include 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; } template consteval bool valid_object_router() { static_assert(requires { typename RRouter::ObjectType; }, "Router type must provide typename Router::ObjectType"); static_assert(valid_object_traits, "Router::ObjectType must satisfy objectmodel traits"); static_assert(std::is_standard_layout_v, "Router must have standard laayout, i.e. no virtual methods. Virtual methods belong in OObject::AbstractInterface*>"); return true; }; // ---------------------------------------------------------------- /** Associates an interface with an representation. * Specialize to record such associations. **/ template struct ISpecificFor; /** type-erased implementation of AComplex, see below **/ struct IComplex_Any; /** abstract interface for a complex number **/ struct AComplex { using TypeErasedIface = IComplex_Any; virtual double xcoord(void * data) const = 0; virtual double ycoord(void * data) const = 0; virtual double argument(void * data) const = 0; virtual double magnitude(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 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_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_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_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 struct DOpaquePlaceholder {}; using IComplex_DOpaquePlaceholder = IComplex_Any; template <> struct ISpecificFor { using ImplType = IComplex_Any; }; // ----- Representation: Polar Coordinates ----- /** complex number, represented using polar coordinates **/ struct DPolarCoords { DPolarCoords(double arg, double mag) : arg_{arg}, mag_{mag} {} double arg_; double mag_; }; // ----- AComplex for DPolarCoords ----- /** implementation of AComplex interface with representation DPolarCoords **/ using IComplex_DPolarCoords = IComplex_Specific; template <> double IComplex_Specific::_xcoord(DPolarCoords * data) { return data->mag_ * std::cos(data->arg_); }; template <> double IComplex_Specific::_ycoord(DPolarCoords * data) { return data->mag_ * std::sin(data->arg_); }; template <> double IComplex_Specific::_argument(DPolarCoords * data) { return data->arg_; } template <> double IComplex_Specific::_magnitude(DPolarCoords * data) { return data->mag_; } template <> void IComplex_Specific::_destruct_data(DPolarCoords * data) { data->~DPolarCoords(); } template <> struct ISpecificFor { using ImplType = IComplex_Specific; }; // ----- Representation: Rectangular Coordinates ----- /** complex number, represented using rectangular coordinates **/ struct DRectCoords { DRectCoords(double x, double y) : x_{x}, y_{y} {} double x_; double y_; }; // ----- AComplex for DRectCoords ----- /** implementation of AComplex interface with representation DRectCoords **/ using IComplex_DRectCoords = IComplex_Specific; template <> double IComplex_Specific::_xcoord(DRectCoords * data) { return data->x_; }; template <> double IComplex_Specific::_ycoord(DRectCoords * data) { return data->y_; }; template <> double IComplex_Specific::_argument(DRectCoords * data) { return std::atan(data->y_ / data->x_); } template <> double IComplex_Specific::_magnitude(DRectCoords * data) { double x = data->x_; double y = data->y_; return std::sqrt(x*x + y*y); } template <> void IComplex_Specific::_destruct_data(DRectCoords * data) { data->~DRectCoords(); } template <> struct ISpecificFor { using ImplType = IComplex_Specific; }; // ----- polymorphic box ----- /** * Unqiuely-owned instance with runtime polymorphism. * * Unlike OUniqueBox can use for variant data * without additional overhead. Tradeoff is that avoiding such * overhead excludes std::unique_ptr. * * 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 OUniqueBox { using AbstractInterface = AInterface; using ISpecific = ISpecificFor::ImplType; /* note: Data can be void here */ using DataType = Data; using DataBox = Data*; explicit OUniqueBox() {} /* unsatisfactory b/c doesn't enforce that @p d is heap-allocated */ explicit OUniqueBox(DataBox d) : data_{std::move(d)} {} ~OUniqueBox() { if (data_ != nullptr) { this->iface()->destruct_data(data_); delete data_; this->data_ = nullptr; } } 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 { using ObjectType = Object; RComplex() {} RComplex(Object::DataBox data) : Object{std::move(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()); } /** note: would prefer this to be constexpr, but seems infeasible asof gcc 14.3 **/ static bool _valid; }; template bool RComplex::_valid = valid_object_router(); template requires abstract_interface struct RoutingFor; template struct RoutingFor { using RoutingType = RComplex; }; template using RoutingType = RoutingFor::RoutingType; // ----- unique any; coordinates with OUniqueBox ----- /** boxed object, held by unique-pointer equivalent. * - With default Data argument: * type-erased polymorphic container * - with specific Data argument: * typed container. Trivially de-virtualizable * * Example: * std::unique_ptr z1_in * = std::make_unique(1.0, 0.0): * ubox z1{z1_in.release()}; * z1.xcoord(); * * * +-----+ +-----------------+ * Interface | x-------------->| vtable for | * +-----+ | some descendant | * Data | x--------\ | of AInterface | * +-----+ | | | * | +-----------------+ * | * | +--------------+ * \----->| data :: Repr | * +--------------+ * * 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 * 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 ubox : public RoutingType> { using Super = RoutingType>; ubox() {} explicit ubox(Super::DataBox d) : Super(d) {} /** copy contents of this instance into *dest. **/ void move2any(ubox * dest) { static_assert(sizeof(ubox) == sizeof(ubox)); ::memcpy((void*)dest, (void*)this, sizeof(ubox)); // this is almost right. But would not copy vtable pointer //*dest = *(reinterpret_cast*>(this)); this->data_ = nullptr; } /** move constructor from a different representation. * allowed given: * - same abstract interface * - same strategy (unique / refcounted / ..) **/ template ubox(ubox && other) requires (std::is_same_v || std::is_convertible_v) : Super() { static_assert(sizeof(ubox) == sizeof(ubox)); other.move2any(this); assert(other.data_ = nullptr); } }; } /*namespace*/ // ----- UNIT TESTS ----- TEST_CASE("objectmodel-specific-1", "[objectmodel]") { /* arg=0, mag=1 -> 1+0i */ DPolarCoords polar{0.0, 1.0}; IComplex_Specific polar_iface; REQUIRE(polar_iface._xcoord(&polar) == 1.0); REQUIRE(polar_iface._ycoord(&polar) == 0.0); REQUIRE(polar_iface._argument(&polar) == 0.0); REQUIRE(polar_iface._magnitude(&polar) == 1.0); } TEST_CASE("objectmodel-specific-2", "[objectmodel]") { /* arg=0, mag=1 -> 1+0i */ DRectCoords rect{1.0, 0.0}; IComplex_Specific rect_iface; REQUIRE(rect_iface._xcoord(&rect) == 1.0); REQUIRE(rect_iface._ycoord(&rect) == 0.0); REQUIRE(rect_iface._argument(&rect) == 0.0); REQUIRE(rect_iface._magnitude(&rect) == 1.0); } TEST_CASE("uniquebox-1", "[objectmodel]") { auto tmp = std::make_unique(0.0, 1.0); OUniqueBox box{tmp.release()}; 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{tmp.release()}; REQUIRE(box.xcoord() == 1.0); REQUIRE(box.ycoord() == 0.0); REQUIRE(box.argument() == 0.0); REQUIRE(box.magnitude() == 1.0); } TEST_CASE("routing-type-1", "[objectmodel]") { using Object = OUniqueBox; auto tmp = std::make_unique(0.0, 1.0); RoutingType box{tmp.release()}; REQUIRE(box.xcoord() == 1.0); REQUIRE(box.ycoord() == 0.0); REQUIRE(box.argument() == 0.0); REQUIRE(box.magnitude() == 1.0); } TEST_CASE("ubox-1", "[objectmodel]") { auto tmp = std::make_unique(0.0, 1.0); ubox box{tmp.release()}; REQUIRE(box.xcoord() == 1.0); REQUIRE(box.ycoord() == 0.0); REQUIRE(box.argument() == 0.0); REQUIRE(box.magnitude() == 1.0); } TEST_CASE("ubox-2", "[objectmodel]") { auto tmp = std::make_unique(1.0, 0.0); ubox box{tmp.release()}; REQUIRE(box.xcoord() == 1.0); REQUIRE(box.ycoord() == 0.0); REQUIRE(box.argument() == 0.0); REQUIRE(box.magnitude() == 1.0); } TEST_CASE("ubox-any-1", "[objectmodel]") { /* default ctor */ ubox any; } TEST_CASE("ubox-any-2", "[objectmodel]") { /* equivalent to ubox, but impl doesn't use std::unique_ptr */ ubox any{new DRectCoords{1.0, 0.0}}; REQUIRE(any.xcoord() == 1.0); REQUIRE(any.ycoord() == 0.0); REQUIRE(any.argument() == 0.0); REQUIRE(any.magnitude() == 1.0); } TEST_CASE("ubox-any-3", "[objectmodel]") { /* equivalent to ubox, but impl doesn't use std::unique_ptr */ ubox z1{new DRectCoords{1.0, 0.0}}; DRectCoords * z1_data = z1.data(); REQUIRE(z1.data() != nullptr); REQUIRE(z1.xcoord() == 1.0); /* can type-erase */ ubox z1_any; REQUIRE(z1_any.data() == nullptr); z1.move2any(&z1_any); /* z1 should be empty, it moved itself */ REQUIRE(z1.data() == nullptr); REQUIRE((void*)z1_any.data() == (void*)z1_data); REQUIRE(z1_any.xcoord() == 1.0); } } /*namespace ut*/ } /*namespace xo*/ /* end objectmodel.test.cp */