xo-alloc2: scaffold for interface+data separation

This commit is contained in:
Roland Conybeare 2025-12-07 15:55:33 -05:00
commit ffd391adef
4 changed files with 298 additions and 31 deletions

View file

@ -3,13 +3,15 @@
# Relative to xo-alloc:
1. keep interface and data separate.
1a. `Representation` classes. Entirely passive; strictly no methods.
1a. *Representation* or *Data* classes. Entirely passive; strictly no methods.
motivation: data doesn't carry any linker-dependency baggage;
it's just layout.
example:
struct RPolar { double arg; double mag; };
struct RRect { double x; double y; };
```
struct DPolar { double arg; double mag; };
struct DRRect { double x; double y; };
```
1b. `Interface` classes. These have abstract methods only.
motivation: for runtime polymorphism, specify interface
@ -18,7 +20,8 @@
as first argument.
example:
struct IComplex {
```
struct AComplex {
using repr_type = void;
virtual double xcoord(void * repr) const = 0;
@ -26,6 +29,7 @@
virtual double magnitude(void * repr) const = 0;
virtual double argument(void * repr) const = 0;
};
```
1c. `Implementation` classes. Implement a specific interface (as in 1b)
for a specific data representation (as in 1a).
@ -36,7 +40,7 @@
example:
```
struct Complex_Rect {
struct IComplex_Rect : public AComplex {
using repr_type = RRect;
double _xcoord(RRect * repr) const { return repr->x; }
@ -60,7 +64,7 @@
double argument(void * repr) const final override;
};
struct Complex_Polar {
struct IComplex_Polar : public AComplex {
using repr_type = RPolar;
// implement IComplex for RPolar
@ -68,70 +72,111 @@
};
```
Here `IComplex_Rect` and `IComplex_Polar` are constructible.
They're concrete in the sense that they expect a specific representation
(`IComplex_Rect::repr_type`, `IComplex_Polar::repr_type` respectively).
1d. `Object` classes. Pair implementation and interface.
May use smart pointer here to express strategy for managing
memory used for representation. Don't expect to need this for
interfaces, since interface content entirely known at compile time.
example:
```
// borrowed
struct _Complex_Rect : public Complex_Rect {
bp<RRect> repr; // naked pointer
struct OComplex_Rect : public IComplex_Rect {
DRect * data() const { return data_; }
bp<DRect> data_; // naked pointer
};
struct _Complex_Polar : public Complex_Polar {
bp<RPolar> repr;
struct OComplex_Polar : public IComplex_Polar {
DPolar * data() const { return data_; }
bp<DPolar> data_;
};
// unique
struct _Complex_Rect : public Complex_Rect {
up<RRect> repr; // unique_ptr
struct OComplex_Rect : public IComplex_Rect {
DRect * data() const { return data_; }
up<DRect> data_; // unique_ptr
};
..
```
Can do this generically.
```
// in bx: 'b' short for 'borrowed' as in unowned.
// 'x' just to distinguish from 'pointer'.
//
template <typename Iface,
typename Repr = typename Iface::repr_type>
struct bxp : public Iface {
struct bx : public Iface {
explicit bx(Repr * data) : data_{data} {}
Repr * data() const { return data_; }
bp<Repr> data_;
};
using t1 = bxp<Complex_Rect>;
using t2 = bxp<Complex_Polar>;
etc.
DRect z1_data{1.0, -1.0};
bx<IComplex_Rect> z1{&z1_data};
DPolar z2_data{sqrt(2.0), pi * 8/7};
bx<IComplex_Polar> z2{&z2_data};
```
Then to invoke a method (compile-time polymorphism)
bxp<Complex_Rect> obj;
obj.xcoord(obj.data_); // obj.xcoord()
Or for runtime polymorphism
bxp<IComplex> obj;
obj.xcoord(obj.data_); // obj.xcoord()
```
z1._xcoord(z1.data());
```
1e. Runtime polymorphism
Observe that bxp<Complex_Rect> and bxp<Complex_Polar> have the same
top-level representation.
- Both have iface member that inherits IComplex,
- both have data pointer compatible with their respective iface member
Can have common representation for runtime polymorphism
- `bxp<Complex_Rect>` and `bxp<Complex_Polar>` have the same size
and compatible representation.
- both inherit `IComplex`
- safe to reinterpret cast
```
// type-erased (placeholder, never used)
struct IComplex_Any : public AComplex {
using repr_type = void;
- safe to reinterpret cast to
double xcoord(void * repr) const final override { assert(false); return 0.0; }
};
bx<IComplex_Rect> z1 = ...;
bx<IComplex_Any> z1_any = reinterpret_cast<IComplex_Any>(z1);
```
Capturing the pattern:
```
// in abstract interface
struct AComplex {
using ErasedIfaceType = IComplex_Any;
..
}
template<typename Iface,
typename Repr = typename Iface::repr_type>
struct bx : public Iface {
..
operator bx<Iface::typename ErasedIfaceType>() {
// in particular, overwrites vtable pointer
return reinterpret_cast<bx<Iface::typename ErasedIfaceType>>(*this);
}
..
};
```
2. Remarks
- shared pattern with pimpl idiom,
except impl isn't private
- can use the same Data type with an unrelated interface.
Although lose the automatic assocation
- can put forwarding methods into object structs,
though will be boilerplatey.

View file

@ -0,0 +1,14 @@
# xo-alloc2/utest/CMakeLists.txt
#
set(UTEST_EXE utest.alloc2)
set(UTEST_SRCS
alloc2_utest_main.cpp
objectmodel.test.cpp)
if (ENABLE_TESTING)
xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS})
xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2)
endif()
# end CMakeLists.txt

View file

@ -0,0 +1,6 @@
/* file alloc2_utest_main.cpp */
#define CATCH_CONFIG_MAIN
#include "catch2/catch.hpp"
/* end alloc2_utest_main.cpp */

View file

@ -0,0 +1,202 @@
/** @file objectmodel.test.cpp
*
* @author: Roland Conybeare, Dec 2025
*
* Testing rust-like split iface/data object model
* 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 follow pattern DRepr, 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
**/
#include <catch2/catch.hpp>
#include <cmath>
#include <cassert>
namespace xo {
namespace ut {
namespace {
/** Associates an interface with an representation.
* Specialize to record such associations.
**/
template <typename Interface,
typename Data>
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;
};
/** 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; }
};
template <typename Repr>
struct IComplex_Specific : public AComplex {
double _xcoord(Repr *) const;
double _ycoord(Repr *) const;
double _argument(Repr *) const;
double _magnitude(Repr *) const;
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); }
};
// ----- Polar Coordinates -----
/** complex number, represented using polar coordinates **/
struct DPolarCoords {
DPolarCoords(double arg, double mag) : arg_{arg}, mag_{mag} {}
double arg_;
double mag_;
};
/** implementation of AComplex interface with representation DPolarCoords **/
using struct IComplex_DPolarCoords = IComplex_Specific<DPolarCoords>;
template <>
IComplex_Specific<DPolarCoords>::_xcoord(DPolarCoords * data) const {
return data->mag_ * std::cos(data->arg_);
};
template <>
IComplex_Specific<DPolarCoords>::_ycoord(DPolarCoords * data) const {
return data->mag_ * std::sin(data->arg_);
};
template <>
IComplex_Specific<DPolarCoords>::_argument(DPolarCoords * data) const {
return data->arg_;
}
template <>
IComplex_Specific<DPolarCoords>::_magnitude(DPolarCoords * data) const {
return data->mag_;
}
template <>
struct ISpecificFor<AComplex, DPolarCoords> {
using ImplType = IComplex_Specific<DPolarCoords>;
};
// ----- Rectangular Coordinates -----
/** complex number, represented using rectangular coordinates **/
struct DRectCoords {
DRectCoords(double x, double y) : x_{x}, y_{y} {}
double x_;
double y_;
};
/** implementation of AComplex interface with representation DRectCoords **/
using struct IComplex_DRectCoords = IComplex_Specific<DRectCoords>;
template <>
IComplex_Specific<DRectCoords>::_xcoord(DRectCoords * data) const {
return data->mag_ * std::cos(data->arg_);
};
template <>
IComplex_Specific<DRectCoords>::_ycoord(DRectCoords * data) const {
return data->mag_ * std::sin(data->arg_);
};
template <>
IComplex_Specific<DRectCoords>::_argument(DRectCoords * data) const {
return data->arg_;
}
template <>
IComplex_Specific<DRectCoords>::_magnitude(DRectCoords * data) const {
return data->mag_;
}
template <>
struct ISpecificFor<AComplex, DRectCoords> {
using ImplType = IComplex_Specific<DRectCoords>;
};
template <>
struct ISpecificFor<AComplex, DRectCoords> {
using ImplType = IComplex_Specific<DRectCoords>;
};
// ----- box with unique pointer -----
/** u for unique, b for box. Using lowercase for unobtrusiveness,
* so that in ub<MyType>, MyType is naturally emphasized
*
* @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>::typename ImplType {
Data * data() const { return data_.get(); }
up<Data> data_;
};
template <typename Object>
struct RComplex : public Object {
double xcoord() const { return _xcoord(data()); }
double ycoord() const { return _ycoord(data()); }
double argument() const { return _argument(data()); }
double magnitude() const { return _magnitude(data()); }
}
template <typename AInterface, typename Object>
struct RoutingFor;
template <typename Object>
struct RoutingFor<AComplex, Object> {
using RoutingType = RComplex<Object>;
};
template <typename AInterface, typename Data>
struct ubox : public RoutingFor<AInterface, Data>::typename RoutingType { }
}
}
}
}