xo-alloc2: object model utest: + concepts, streamlining

This commit is contained in:
Roland Conybeare 2025-12-09 15:52:37 -05:00
commit d93cf1734b

View file

@ -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 <typename T>
concept abstract_interface = requires {
std::is_abstract_v<T>,
std::is_polymorphic_v<T>;
/** require no state, just a single vtable pointer **/
sizeof(T) == sizeof(PlaceholderAbstractInterface);
!std::has_virtual_destructor_v<T>;
std::is_trivially_destructible_v<T>;
};
/** For example ISpecific = IComplex_DPolarCoords
**/
template <typename AInterface, typename ISpecific>
concept implements_interface = requires {
std::is_base_of_v<AInterface, ISpecific>;
std::is_default_constructible_v<ISpecific>;
std::is_standard_layout_v<ISpecific>;
/** require no additional state **/
sizeof(ISpecific) == sizeof(AInterface);
};
/** Router delivers data to interface implementation **/
template <typename Object, typename Router>
concept provides_router = requires {
std::is_base_of_v<Object, Router>;
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<AMyInterface>());
*
**/
template <typename T>
consteval bool valid_abstract_interface()
{
static_assert(std::is_abstract_v<T>,
"Abstract interface expected to have all-abstract methods");
static_assert(std::is_polymorphic_v<T>,
"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<T>,
"Abstract interface does not benefit from virtual dtor since no state");
static_assert(std::is_trivially_destructible_v<T>,
"Abstract interface expected to have trivial dtor since no state");
return true;
};
template <typename AInterface, typename ISpecific>
consteval bool valid_interface_implementation()
requires (valid_abstract_interface<AInterface>())
{
static_assert(std::is_base_of_v<AInterface, ISpecific>,
"Interface implementation must inherit abstract interface");
static_assert(std::is_default_constructible_v<ISpecific>,
"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<ISpecific>,
"Interface implementation may does not benefit from virtual dtor since no state");
static_assert(std::is_trivially_destructible_v<ISpecific>,
"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<AInterface, ISpecific>,
// "Interface implementation must directly inherit interface (no base offset)");
return true;
};
template <typename OObject>
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<OObject::AbstractInterface, OObject::ISpecific>,
"Object::ISpecific must implement Object::AbstractInterface");
static_assert(std::is_standard_layout_v<OObject>,
"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<const typename OObject::AbstractInterface*>; },
"Object must have non-virtual method iface() returning const Object::AbstractInterface");
static_assert(requires(const OObject & obj) { { obj.data() } -> std::convertible_to<typename OObject::DataType*>; },
"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<AComplex>();
// ----------------------------------------------------------------
/** 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<AComplex, IComplex_Any>;
// ----------------------------------------------------------------
template <typename Repr>
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 <typename Repr>
bool
IComplex_Specific<Repr>::_valid = valid_interface_implementation<AComplex, IComplex_Specific>;
// ----- 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<DPolarCoords>;
@ -188,8 +328,8 @@ namespace xo {
template <>
void
IComplex_Specific<DPolarCoords>::_destruct(DPolarCoords *) {
/*trivial*/
IComplex_Specific<DPolarCoords>::_destruct_data(DPolarCoords * data) {
data->~DPolarCoords();
}
template <>
@ -197,7 +337,7 @@ namespace xo {
using ImplType = IComplex_Specific<DPolarCoords>;
};
// ----- 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<DRectCoords>;
@ -239,8 +381,8 @@ namespace xo {
template <>
void
IComplex_Specific<DRectCoords>::_destruct(DRectCoords * /*data*/) {
/*trivial*/
IComplex_Specific<DRectCoords>::_destruct_data(DRectCoords * data) {
data->~DRectCoords();
}
template <>
@ -248,40 +390,6 @@ namespace xo {
using ImplType = IComplex_Specific<DRectCoords>;
};
// ----- 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<AComplex, DRectCoords>
*
* Example:
* OUniqueBox<AComplex, DRectCoords> z1 = ..;
* z1._xcoord(z1.data());
**/
template <typename AInterface, typename Data>
struct OUniqueBox : ISpecificFor<AInterface, Data>::ImplType {
using DataType = Data;
using DataBox = std::unique_ptr<Data>;
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 <typename AInterface, typename Data = DOpaquePlaceholder>
struct OUniqueAny : ISpecificFor<AInterface, Data>::ImplType {
struct OUniqueBox {
using AbstractInterface = AInterface;
using ISpecific = ISpecificFor<AInterface, Data>::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<OUniqueAny> **/
const AInterface * iface() const
requires std::is_same_v<Data, DOpaquePlaceholder>
{
return std::launder(&iface_);
}
const AInterface * iface() const
requires (!std::is_same_v<Data, DOpaquePlaceholder>)
{
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<OUniqueBox> **/
Data * data() const { return data_; }
ISpecific iface_;
DataBox data_ = nullptr;
};
template <typename AInterface, typename Data>
bool
OUniqueBox<AInterface, Data>::_valid = valid_object_traits<OUniqueBox>();
// ----- Router; RFoo pairs with AFoo -----
/** For example, inherit OUniqueBox<AComplex>
**/
template <typename Object>
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 <typename AInterface, typename Object>
@ -344,29 +479,14 @@ namespace xo {
template <typename AInterface, typename Object>
using RoutingType = RoutingFor<AComplex, Object>::RoutingType;
// ----- unique box; coordinates with OUniqueBox -----
/** boxed object, held by unique pointer
*
* Example:
* ubox<AComplex, DRectCoords> z1 = ..;
* z1.xcoord();
**/
template <typename AInterface, typename Data>
struct ubox : public RoutingType<AComplex, OUniqueBox<AComplex, Data>> {
using Super = RoutingType<AComplex, OUniqueBox<AComplex, Data>>;
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<DRectCoords> z1_in
* = std::make_unique<DRectCoords>(1.0, 0.0):
* uany<AComplex> z1{z1_in.release()};
* ubox<AComplex> z1{z1_in.release()};
* z1.xcoord();
*
*
@ -381,32 +501,32 @@ namespace xo {
* \----->| data :: Repr |
* +--------------+
*
* Binary representaiton of unay<AInterface, Data>
* Binary representation of unay<AInterface, Data>
* is compatible for different values of @tparam Data
* as long as vtable pointer moves along with data pointer.
*
* In particular binary representation for
* uany<AInterface,D> is as if it inherited uany<AInterface>
* ubox<AInterface,D> is as if it inherited ubox<AInterface>
* (even though it does not as far as compiler is concerned)
*
* This is load-bearing for @ref move2any see below
**/
template <typename AInterface, typename Data = DOpaquePlaceholder>
struct uany : public RoutingType<AComplex, OUniqueAny<AComplex, Data>> {
using Super = RoutingType<AComplex, OUniqueAny<AComplex, Data>>;
struct ubox : public RoutingType<AComplex, OUniqueBox<AComplex, Data>> {
using Super = RoutingType<AComplex, OUniqueBox<AComplex, Data>>;
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<AInterface> * dest) {
static_assert(sizeof(uany<AInterface>)
== sizeof(uany<AInterface, Data>));
void move2any(ubox<AInterface> * dest) {
static_assert(sizeof(ubox<AInterface>)
== sizeof(ubox<AInterface, Data>));
::memcpy((void*)dest, (void*)this, sizeof(uany<AInterface>));
// this is almost right. But doesn't copy vtable pointer
//*dest = *(reinterpret_cast<uany<AInterface>*>(this));
::memcpy((void*)dest, (void*)this, sizeof(ubox<AInterface>));
// this is almost right. But would not copy vtable pointer
//*dest = *(reinterpret_cast<ubox<AInterface>*>(this));
this->data_ = nullptr;
}
@ -416,13 +536,13 @@ namespace xo {
* - same strategy (unique / refcounted / ..)
**/
template <typename Data2>
uany(uany<AInterface, Data2> && other)
ubox(ubox<AInterface, Data2> && other)
requires (std::is_same_v<Data, DOpaquePlaceholder>
|| std::is_convertible_v<Data2*, Data>)
: Super()
{
static_assert(sizeof(uany<AInterface, Data2>)
== sizeof(uany<AInterface, Data>));
static_assert(sizeof(ubox<AInterface, Data2>)
== sizeof(ubox<AInterface, Data>));
other.move2any(this);
@ -460,20 +580,21 @@ namespace xo {
TEST_CASE("uniquebox-1", "[objectmodel]")
{
OUniqueBox<AComplex, DPolarCoords> box
{std::make_unique<DPolarCoords>(0.0, 1.0)};
auto tmp = std::make_unique<DPolarCoords>(0.0, 1.0);
OUniqueBox<AComplex, DPolarCoords> 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<AComplex, DPolarCoords>;
auto tmp = std::make_unique<DPolarCoords>(0.0, 1.0);
RComplex<Object> box{std::make_unique<DPolarCoords>(0.0, 1.0)};
RComplex<Object> 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<AComplex, DPolarCoords>;
auto tmp = std::make_unique<DPolarCoords>(0.0, 1.0);
RoutingType<AComplex, Object> box{std::make_unique<DPolarCoords>(0.0, 1.0)};
RoutingType<AComplex, Object> 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<AComplex,DPolarCoords> box{std::make_unique<DPolarCoords>(0.0, 1.0)};
auto tmp = std::make_unique<DPolarCoords>(0.0, 1.0);
ubox<AComplex,DPolarCoords> 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<AComplex,DRectCoords> box{std::make_unique<DRectCoords>(1.0, 0.0)};
auto tmp = std::make_unique<DRectCoords>(1.0, 0.0);
ubox<AComplex,DRectCoords> 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<AComplex> any;
ubox<AComplex> any;
}
TEST_CASE("uany-2", "[objectmodel]")
TEST_CASE("ubox-any-2", "[objectmodel]")
{
/* equivalent to ubox<AComplex,DRectCoords>, but impl doesn't use std::unique_ptr */
uany<AComplex,DRectCoords> any{new DRectCoords{1.0, 0.0}};
ubox<AComplex,DRectCoords> 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<AComplex,DRectCoords>, but impl doesn't use std::unique_ptr */
uany<AComplex,DRectCoords> z1{new DRectCoords{1.0, 0.0}};
ubox<AComplex,DRectCoords> 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<AComplex> z1_any;
ubox<AComplex> z1_any;
REQUIRE(z1_any.data() == nullptr);