xo-facet: header impl + docs

This commit is contained in:
Roland Conybeare 2025-12-10 20:17:21 -05:00
commit ec4ecb29aa
15 changed files with 1138 additions and 36 deletions

View file

@ -17,7 +17,7 @@ author = 'Roland Conybeare'
extensions = [ "breathe",
"sphinx.ext.mathjax", # inline math
"sphinx.ext.autodoc", # generate info from docstrings
# "sphinxcontrib.ditaa", # diagrams-through-ascii-art
"sphinxcontrib.ditaa", # diagrams-through-ascii-art
"sphinxcontrib.plantuml", # text -> uml diagrams
]

View file

@ -471,7 +471,25 @@ macro(xo_docdir_sphinx_config rst_files)
if (XO_ENABLE_DOCS)
if (XO_SUBMODULE_BUILD)
# in submodule build, rely on toplevel docs/CMakeLists.txt file instead
# in submodule build, rely on toplevel docs/CMakeLists.txt file instead.
#
# translate ${rst_files} to absolute paths
#
set(SPHINX_ABS_RST_FILES)
foreach(rst_file ${SPHINX_RST_FILES})
get_filename_component(
abs_path "${rst_file}"
ABSOLUTE
BASE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
list(APPEND SPHINX_ABS_RST_FILES "${abs_path}")
endforeach()
# append to global property
set_property(GLOBAL APPEND
PROPERTY XO_UMBRELLA_SPHINX_RST_FILES
${SPHINX_ABS_RST_FILES})
message(STATUS "SPHINX_ABS_RST_FILES=${SPHINX_ABS_RST_FILES}")
else()
# build docs starting from here only in standalone build.
# otherwise use top-level doxygen setup.
@ -529,6 +547,7 @@ endmacro()
# config for an umbrella project that composes standalone subprojects
#
macro(xo_umbrella_sphinx_config rst_files)
# here SPHINX_RST_FILES refers to toplevel-only .rst files in umbrella project
list(APPEND SPHINX_RST_FILES ${rst_files})
foreach(arg IN ITEMS ${ARGN})
list(APPEND SPHINX_RST_FILES ${arg})
@ -539,12 +558,17 @@ macro(xo_umbrella_sphinx_config rst_files)
find_program(SPHINX_EXECUTABLE NAMES sphinx-build REQUIRED)
message(STATUS "SPHINX_EXECUTABLE=${SPHINX_EXECUTABLE}")
get_property(SPHINX_ABS_RST_FILES GLOBAL PROPERTY XO_UMBRELLA_SPHINX_RST_FILES)
message(STATUS "SPHINX_ABS_RST_FILES=${SPHINX_ABS_RST_FILES}")
set(SPHINX_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}/sphinx/html)
set(SPHINX_INDEX_FILE ${SPHINX_OUTPUT_DIR}/index.html)
# root of sphinx doc tree
set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR})
set(SPHINX_DEPS doxygen_${PROJECT_NAME} conf.py ${SPHINX_RST_FILES} ${SPHINX_RST_FILES_GLOB} ${DOX_DEPS})
# SPHINX_RST_FILES: top-level .rst files in umbrella project
# SPHINX_ABS_RST_FILES: satellite .rst files, collected via XO_SUBMODULE_BUILD, rewritten to absoluate paths
set(SPHINX_DEPS doxygen_${PROJECT_NAME} conf.py ${SPHINX_RST_FILES} ${SPHINX_ABS_RST_FILES} ${DOX_DEPS})
add_custom_command(
OUTPUT ${SPHINX_INDEX_FILE}

View file

@ -7,7 +7,7 @@ xo_docdir_sphinx_config(
glossary.rst
#install.rst
#introduction.rst
#implementation.rst
implementation.rst
)
# see xo-reader/doc or xo-unit/doc for working examples

View file

@ -6,3 +6,6 @@ Glossary
.. glossary::
fomo
| facet object model
xfer
| abbreviation for transfer

View file

@ -0,0 +1,93 @@
.. _implementation:
Components
==========
Library dependency tower for *xo-facet*:
.. ditaa::
+-----------------+
| xo_facet |
+-----------------+
| xo_cmake |
+-----------------+
Abstraction tower for *xo-facet* components.
.. ditaa::
:--scale: 0.85
+--------------------------------+
| obj(A,D) |
+--------------------------------+
| RRouter(A,D) |
+--------------------------------+
| OObject(A,D) |
+--------------------------------+
| FacetImplmentationType(A,D) |
+----------------+---------------+
| facet [A] | data [D] |
+----------------+---------------+
Decorated with sample method calls, to reveal type recovery
.. ditaa::
:--scale: 0.85
+--------------------------------+
| obj(A,D) | x.foo()
+--------------------------------+
| RRouter(A,D) | x.foo()
+--------------------------------+
| OObject(A,D) | x.iface_.foo(x.data_)
+--------------------------------+
| FacetImplmentationType(A,D) | x.foo(void*data)
+----------------+---------------+
| facet A | data D | virtual x.foo(void* data)
+----------------+---------------+
.. list-table:: Descriptions
:header-rows: 1
:widths: 30 30 60
* - Component
- Use
- Description
* - obj<A,D>
- x.foo()
- convenience wrapper with interface A, with state D*
* - RRouter<A,D>
- x.foo()
- auto injects data pointer
* - OObject<A,D>
- x.iface()->foo(x.data(), ..)
- fat object pointer. combine i/face + data pointer.
* - FacetImplementationType<A,D>
- x.foo(void* data, ..)
- implement facet for a particular state datatype;
explicit type-erased state
* - facet
- x.foo(void* data, ..)=0
- fully abstract interface; explicit type-erased state
.. uml::
:caption: fat-object-pointer layout
:scale: 99%
:align: center
object z1<<obj>>
z1 : iface = vt1
z1 : data = d1
object vt1<<interface>>
vt1 : foo()
vt1 : bar()
object d1<<data>>
d1 : x = 0.6
d1 : y = 0.8
z1 o-- vt1
z1 o-- d1

View file

@ -49,6 +49,7 @@ This gives us several benefits:
:maxdepth: 2
:caption: xo-facet contents
implementation
glossary
genindex
search

View file

@ -0,0 +1,193 @@
/** @file OObject.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include "facet_implementation.hpp"
#include "typeseq.hpp"
#include <new>
#include <cstring>
namespace xo {
namespace facet {
template <typename OObject>
consteval bool valid_object_traits()
{
static_assert(requires { typename OObject::AbstractInterface; },
"OObject type must provide typename Object::AbstractInterface");
static_assert(requires { typename OObject::ISpecific; },
"OObject type must provide typename Object::ISpecific");
static_assert(requires { typename OObject::DataType; },
"OObject type must provide typename Object::DataType");
static_assert(valid_facet_implementation<OObject::AbstractInterface, OObject::ISpecific>,
"OObject::ISpecific must implement Object::AbstractInterface");
static_assert(std::is_standard_layout_v<OObject>,
"OObject 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*>; },
"OObject must have non-virtual method iface()"
" returning const OObject::AbstractInterface");
static_assert(requires(const OObject & obj) {
{ obj.data() } -> std::convertible_to<typename OObject::DataType*>; },
"OObject must have non-virtual method data() returning OObject::DataType*");
return true;
}
/** A "fat object pointer": combines two pointers:
*
* 1. behavior: an interface pointer
* (implementation of @tparam AFacet a.k.a. vtable pointer)
* for passive state DRepr.
* Interface pointers are static globals.
*
* 2. state: a data pointer to instance of passive state DRepr
* An OObject instance does not own its data pointer
*
* Performance note: when DRepr can be determined at compile time,
* it's often feasible to optimize away the interface part.
*
* Runtime polymorphism when @tparam DRepr is @ref DVariantPlaceholder
*
* Application code should not use this directly.
* Instead, inherit a facet-specific routing wrapper that automatically
* injects @ref data as first argument to @ref iface_ methods.
**/
template <typename AFacet, typename DRepr = DVariantPlaceholder>
struct OObject {
using FacetType = AFacet;
using ISpecific = FacetImplType<AFacet, DRepr>;
using DataType = DRepr;
using DataPtr = DRepr*;
explicit OObject() {}
explicit OObject(DataPtr d) : data_{d} {}
/** trivial: nothing to do for @ref iface_ and does not own @ref data_ **/
~OObject() = default;
/** OObject is truthy **/
operator bool() const { return data_ != nullptr; }
/** interface pointer for variant OObject instances.
* These instance support runtime polymorphism.
**/
const FacetType * iface() const
requires std::is_same_v<DataType, DVariantPlaceholder>
{
/* std::launder:
*
* contents of iface_ at runtime will not match
* compile-time datatype. This prohibits compiler de-virtualizing
* calls to ISpecific methods, based on mistaken belief that
* vtable pointer is known at compile time.
*/
return std::launder(&iface_);
}
/** interface pointer for OObject instance with representation
* known at compile time.
*
* Calls here should be straightforward to devirtualize
**/
const FacetType * iface() const
requires (!std::is_same_v<DataType, DVariantPlaceholder>)
{
/* don't use std::launder: want compiler to devirtualize
* calls to virtual @ref iface_ methods
*/
return &iface_;
}
DataPtr data() const { return data_; }
void reset() { data_ = nullptr; }
/**
* We're either:
* - assigning from pointer with compatible representation
* - implementing the fat-object-pointer equivalent of
* assigning a derived pointer to a base pointer.
**/
template <typename DOther>
OObject & from_data(DOther * other) {
static_assert(std::is_same_v<DRepr, DVariantPlaceholder>
|| std::is_convertible_v<DOther*, DRepr*>);
if constexpr (std::is_convertible_v<DOther*, DRepr*>) {
/** assigning from data with same representation **/
this->data_ = other;
} else /*DRepr is DVariantPlaceholder*/ {
/** assigning to variant **/
/* acquire fat object pointer for (AFacet, DOther) */
OObject<AFacet, DOther> oother(other);
static_assert(sizeof(*this) == sizeof(oother));
::memcpy((void*)this, (void*)&oother, sizeof(*this));
}
return *this;
}
/**
* Downcast to pointer of type DOther*, if valid.
* Provided when actual type of @ref data_ is not DRepr,
* because DRepr is DVariantPlaceholder.
*
* We can't rely on dynamic_cast here, because DRepr's
* don't need to be related as far as c++ type system is
* concerned.
**/
template <typename DOther>
DOther * downcast()
requires (std::is_same_v<DataType, DVariantPlaceholder>)
{
if (data_ && (typeseq::id<DOther>() == this->iface()->_typeseq())) {
/* actual runtime type for data_ is DOther,
* safe to reinterpret
*/
return reinterpret_cast<DOther*>(data_);
} else {
return nullptr;
}
}
DRepr & operator*() { return *data_; }
#ifdef NOT_IN_USE
// not sure if this is a good idea. could just as well write
// auto obj = ...;
// *obj.data() == rhs
/** assign contents of rhs in-place **/
OObject & operator=(const DRect & rhs) {
assert(data_);
*(this->data_) = rhs;
return *this;
}
#endif
/** fetch data pointer. load-bearing for routing classes **/
static bool _valid;
/** runtime interface for this object **/
ISpecific iface_;
/** runtime state for this object **/
DataPtr data_ = nullptr;
};
template <typename AFacet, typename DRepr>
bool
OObject<AFacet, DRepr>::_valid = valid_object_traits<OObject>();
} /*namespace facet*/
} /*namespace xo*/
/* end OObject.hpp */

View file

@ -0,0 +1,74 @@
/** @file OUniqueBox.hpp
*
* @author Roland Conybeare, Dec 2025
**/
namespace xo {
namespace facet {
/**
* Uniquely-owned instance with runtime polymorphism.
*
* Reminder that in the facet object model we expect
* objects to be transient.
*
*
* Unlike OUniqueBox<AInterface, ..> 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 <typename AInterface, typename Data = DOpaquePlaceholder>
struct OUniqueBox {
using AbstractInterface = AInterface;
using ISpecific = ISpecificFor<AInterface, Data>::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<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;
};
}
} /*namespace xo*/
/* end OUniqueBox.hpp */

View file

@ -0,0 +1,51 @@
/** @file RRouter.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include "OObject.hpp"
namespace xo {
namespace facet {
template <typename RRouter>
consteval bool valid_object_router()
{
static_assert(requires { typename RRouter::ObjectType; },
"Router type must provide typename Router::ObjectType");
static_assert(valid_object_traits<RRouter::ObjectType>,
"Router::ObjectType must satisfy objectmodel traits");
static_assert(std::is_standard_layout_v<RRouter>,
"Router must have standard laayout, i.e. no virtual methods."
" Virtual methods belong in OObject::AbstractInterface*>");
return true;
};
/**
* template <typename Object>
* struct RMyFacet : public Object {
* using ObjectType = Object;
*
* RObject() = default;
* RObject(Object::DataPtr data) : Object{data} {}
*
* void something() const { return Object::iface()->something(Object::data()); }
* int andalso(double somearg) const { return Object::iface()->andalso(Object::data(), somearg); }
* };
*
* template <typename Object>
* struct RoutingFor<AMyFacet, Object> {
* using RoutingType = RMyFacet<Object>;
* };
**/
template <typename AFacet, typename Object>
requires abstract_facet<AFacet>
struct RoutingFor;
template <typename AFacet, typename Object>
using RoutingType = RoutingFor<AFacet, Object>::RoutingType;
}
} /*namespace xo*/
/* end RRouter.hpp */

View file

@ -7,10 +7,16 @@
#include <concepts>
#include <type_traits>
#include <cstdint>
namespace xo {
namespace facet {
// ----- abstract facet -----
namespace detail {
/** aux class to support facet validation
* see @ref abstract_facet, @ref valid_abstract_facet
**/
struct PlaceholderAbstractInterface {
virtual double foo(void * data) const = 0;
};
@ -35,10 +41,13 @@ namespace xo {
/** Use: when defining an abstract facet AMyFacet
*
* struct AMyFacet {
* virtual void foo(void * data) const = 0;
* virtual void something(void * data) const = 0;
* virtual int andalso(void * data, double somearg) const = 0;
*
* static bool _valid;
* };
*
* static_assert(valid_abstract_facet<AMyFacet>());
* bool AMyFacet::_valid = valid_abstract_facet<AMyFacet>();
*
**/
template <typename T>
@ -59,6 +68,10 @@ namespace xo {
static_assert
(std::is_trivially_destructible_v<T>,
"Abstract facet expected to have trivial dtor since no state");
static_assert
(requires(const T & facet) {
{ facet._typeseq() } -> std::convertible_to<std::int32_t>; },
"Abstract facet must provide a _typeseq() method for safe downcasting");
return true;
};

View file

@ -0,0 +1,122 @@
/** @file facet_implementation.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
//#include "xo/facet/facet.hpp"
#include <concepts>
#include <type_traits>
namespace xo {
namespace facet {
// ----- facet implementation -----
// An implementation provides behavior (i.e. code, not data)
// for a particular abstract facet, specialized for a specific data type
/** For example ISpecific = IComplex_DPolarCoords
**/
template <typename AFacet, typename ISpecific>
concept implements_facet = requires {
std::is_base_of_v<AFacet, ISpecific>;
std::is_default_constructible_v<ISpecific>;
std::is_standard_layout_v<ISpecific>;
/** require no additional state **/
sizeof(ISpecific) == sizeof(AFacet);
};
/** Use: when defining datatype recovery for a typed facet implementation:
*
* template <typename Repr>
* struct AMyFacet_Impl : public AMyFacet {
* static void _something(Repr *);
* static int _andalso(Repr *, double somearg);
*
* virtual void something(void * data) const final override {
* _something((Repr *)data);
* }
*
* virtual int andalso(void * data, double somearg) const final override {
* _andalso((Repr *)data, somearg);
* }
*
* static bool _valid;
* };
*
* template <typename Repr>
* bool AMyFacet_Impl<Repr>::_valid
* = valid_facet_implementation<AMyFacet, AMyFacet_Specific>();
**/
template <typename AFacet, typename ISpecific>
consteval bool valid_facet_implementation()
requires (valid_abstract_facet<AFacet>())
{
static_assert(std::is_base_of_v<AFacet, ISpecific>,
"Facet implementation must inherit FacetRttiShim<AFacet>");
static_assert(std::is_default_constructible_v<ISpecific>,
"Facet implementation must be default-constructible");
static_assert(sizeof(ISpecific) == sizeof(AFacet),
"Facet implementation may not introduce state");
static_assert(!std::has_virtual_destructor_v<ISpecific>,
"Facet implementation does not benefit from virtual dtor (since has no data)");
static_assert(std::is_trivially_destructible_v<ISpecific>,
"Facet implementation expected to have trivial dtor (since has no data)");
// don't need this test, it's covered by sizeof check
//static_assert(std::is_pointer_interconvertible_base_of_v<AFacet, ISpecific>,
// "Interface implementation must directly inherit interface (no base offset)");
return true;
};
/** Compile-time facet implementation lookup
*
* @c FacetImplementation<AMyFacet, DSomeRepr>::ImplType
* gives the type that implements @c AMyFacet with state @c DSomeRepr
*
* template<DRepr>
* struct FacetImplementation<AMyFacet, DRepr> {
* using ImplType = IMyFacet_Impl<DRepr>;
* };
*
* template<>
* struct FacetImplementation<AMyFacet, DVariantPlaceholder> {
* using ImplType = IMyFacet_Any;
* };
*
**/
template <typename AFacet, typename DRepr>
struct FacetImplementation {};
/** Retrieve facet implementation for a (facet, datatype) pair **/
template <typename AFacet, typename DRepr>
using FacetImplType = FacetImplementation<AFacet, DRepr>::ImplType;
/** Data type for facet implementation that supports runtime polymorphism.
* Implementation will stub all methods, since they will never be invoked.
*
* template <>
* struct IMyFacet_Any : public AMyFacet {
* virtual void something(void * data) const final override { assert(false); }
* virtual int andalso(void * data) const final override { assert(false); return 0; }
*
* static bool _valid;
* }
*
* template <>
* bool IMyFacet_Any::_valid
* = valid_facet_implementation<AMyFacet, IMyFacet_Any>();
**/
struct DVariantPlaceholder {};
/** PLAN:
* expect also will need runtime version of FacetImplementation.
**/
} /*namespace facet*/
} /*namespace xo*/
/* end facet_implementation.hpp */

View file

@ -0,0 +1,30 @@
/** @file facet_rtti.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#include "facet.hpp"
#include <cstdint>
namespace xo {
namespace facet {
/** CRTP here
*
* to be able to recover original type from a variant,
* we need some analog of dynamic_cast<>.
* We don't use c++ dynamic cast here because we
* are refusing to impose any restrictions on
* facet representation types, so in particular
* they will likely be of unrelated type.
*
* Instead rely on the interface pointer to access
* information about the runtime type associated
* with a stored fat-object-pointer representation.
**/
template <typename AFacet>
struct FacetRttiShim : public AFacet {
};
} /*namespace facet*/
} /*namespace xo*/
/* end facet_rtti.hpp */

View file

@ -0,0 +1,101 @@
/** @file obj.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include "RRouter.hpp"
namespace xo {
namespace facet {
/** object with borrowed state pointer
* - With default Data argument:
* type-erased polymorphic container
* - with specific Data argument:
* typed container. Trivially de-virtualizable
*
* Example:
* std::unique_ptr<DRectCoords> z1_in
* = std::make_unique<DRectCoords>(1.0, 0.0):
* ubox<AComplex> z1{z1_in.release()};
* z1.xcoord();
*
*
* +-----+ +-----------------+
* Interface | x-------------->| vtable for |
* +-----+ | some descendant |
* Data | x--------\ | of AInterface |
* +-----+ | | |
* | +-----------------+
* |
* | +--------------+
* \----->| data :: Repr |
* +--------------+
*
* 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
* 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 AFacet, typename DRepr = DVariantPlaceholder>
struct obj : public RoutingType<AFacet, OObject<AFacet, DRepr>> {
using Super = RoutingType<AFacet, OObject<AFacet, DRepr>>;
obj() {}
explicit obj(Super::DataPtr d) : Super(d) {}
/** copy constructor **/
template <typename DOther>
obj(const obj<AFacet, DOther> && other) : Super()
{
if constexpr (std::is_convertible_v<DRepr, DOther>) {
this->data_ = other.data_;
} else if constexpr (std::is_same_v<DRepr, DVariantPlaceholder>) {
this->from_data(other.data_);
} else {
/* still need something for downcasting */
static_assert(false, "expect DOther compatible with DRepr");
}
}
/** move constructor from a different representation.
* allowed given:
* - same abstract interface
* - same strategy for holding state (naked / unique / refcounted ...)
**/
template <typename DOther>
obj(const obj<AFacet, DOther> && other)
requires (std::is_same_v<DRepr, DVariantPlaceholder>
|| std::is_convertible_v<DOther*, DRepr>)
: Super()
{
static_assert(sizeof(obj<AFacet, DOther>)
== sizeof(obj<AFacet, DRepr>));
other.move2any(this);
assert(other.data_ = nullptr);
}
/** safe downcast from variant. null if downcast fails **/
static obj from(const OObject<AFacet> & other) {
return obj(other.template downcast<DRepr>());
}
};
template <typename AFacet, typename DRepr>
inline obj<AFacet, DRepr>
with_facet(DRepr * data) {
return obj<AFacet, DRepr>(data);
}
} /*namespace facet*/
} /*namespace xo*/
/* end obj.hpp */

View file

@ -0,0 +1,53 @@
/** @file typeseq.hpp
*
* @author Roland Conybeare, Dec 2025
**/
#pragma once
#include <cstdint>
namespace xo {
namespace facet {
/**
* Tag here so we can preserve header-only implementation
* and still have static variable
*/
template<typename Tag = class typeseq_tag>
struct typeseq_impl {
/** Can't have this be constexpr.
* We need ids in shared libraries to be generated
* at load time to avoid false positives
*
* Return unique id number for each type.
* Numbers are sequentially allocated, so can use
* as vector indices
*
* Conversely note that built-in typeinfo may
* return false negatives across library boundaries
* when using clang.
**/
template <typename T>
static int32_t id() {
static bool armed = true;
static int32_t id = 0;
if (armed) {
armed = false;
id = ++s_next_id;
}
return id;
}
static int32_t s_next_id;
};
template <typename Tag>
int32_t typeseq_impl<Tag>::s_next_id = 0;
using typeseq = typeseq_impl<>;
}
} /*namespace xo*/
/* end typeseq.hpp */

View file

@ -3,58 +3,402 @@
* @author Roland Conybeare, Dec 2025
**/
#include "xo/facet/AFacet.hpp"
#include "xo/facet/facet.hpp"
#include "xo/facet/facet_implementation.hpp"
#include "xo/facet/OObject.hpp"
#include "xo/facet/RRouter.hpp"
#include "xo/facet/typeseq.hpp"
#include "xo/facet/obj.hpp"
#include <catch2/catch.hpp>
#include <cmath>
#include <numbers>
#include <cassert>
#include <cstring>
namespace xo {
using xo::facet::valid_abstract_facet;
using xo::facet::valid_facet_implementation;
using xo::facet::FacetImplementation;
using xo::facet::DVariantPlaceholder;
using xo::facet::OObject;
using xo::facet::valid_object_router;
using xo::facet::RoutingType;
using xo::facet::typeseq;
using xo::facet::obj;
using xo::facet::with_facet;
// ------ AComplex -----
/** abstract interface for a complex number **/
struct AComplex {
using TypeErasedIface = struct IComplex_Any;
/** RTTI: reports unique id# for actual runtime data representation **/
virtual int32_t _typeseq() const = 0;
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;
static bool _valid;
};
bool
AComplex::_valid = valid_abstract_facet<AComplex>();
// ----- IComplex_Impl -----
template <typename DRepr>
struct IComplex_Impl;
template <typename DRepr>
struct IComplex_Xfer : public AComplex {
// parallel interface to AComplex, but with specific data type
using Impl = IComplex_Impl<DRepr>;
// from FacetRttiShim<AComplex>
virtual int32_t _typeseq() const { return s_typeseq; }
// from AComplex
virtual double xcoord(void * data) const final override { return Impl::xcoord(*(DRepr*)data); }
virtual double ycoord(void * data) const final override { return Impl::ycoord(*(DRepr*)data); }
virtual double argument(void * data) const final override { return Impl::argument(*(DRepr*)data); }
virtual double magnitude(void * data) const final override { return Impl::magnitude(*(DRepr*)data); }
virtual void destruct_data(void * data) const final override { Impl::destruct_data(*(DRepr*)data); }
static int32_t s_typeseq;
static bool _valid;
};
template <typename DRepr>
int32_t
IComplex_Xfer<DRepr>::s_typeseq = typeseq::id<DRepr>();
template <typename DRepr>
bool
IComplex_Xfer<DRepr>::_valid = valid_facet_implementation<AComplex, IComplex_Xfer>;
namespace facet {
template <typename DRepr>
struct FacetImplementation<AComplex, DRepr> {
using ImplType = IComplex_Xfer<DRepr>;
};
}
// ----- IComplex_Any -----
/** 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 int32_t _typeseq() const { return s_typeseq; }
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); }
static int32_t s_typeseq;
static bool _valid;
};
int32_t
IComplex_Any::s_typeseq = typeseq::id<DVariantPlaceholder>();
bool
IComplex_Any::_valid = valid_facet_implementation<AComplex, IComplex_Any>();
namespace facet {
template <>
struct FacetImplementation<AComplex, DVariantPlaceholder> {
using ImplType = IComplex_Any;
};
}
// ----------------------------------------------------------------
// AComplex, DPolarCoords
//
// complex number represented using polar coordinates (arg, mag)
// ----------------------------------------------------------------
struct DPolarCoords {
DPolarCoords(double arg, double mag) : arg_{arg}, mag_{mag} {}
double arg_;
double mag_;
};
using IComplex_DPolarCoords = IComplex_Xfer<DPolarCoords>;
template <>
struct IComplex_Impl<DPolarCoords> {
static double xcoord(DPolarCoords & self) { return self.mag_ * std::cos(self.arg_); }
static double ycoord(DPolarCoords & self) { return self.mag_ * std::sin(self.arg_); }
static double argument(DPolarCoords & self) { return self.arg_; }
static double magnitude(DPolarCoords & self) { return self.mag_; }
static void destruct_data(DPolarCoords & self) { self.~DPolarCoords(); }
};
// ----------------------------------------------------------------
// AComplex, DRectCoords
//
// complex number represented using rectangular coordinates (x, y)
// ----------------------------------------------------------------
struct DRectCoords {
DRectCoords(double x, double y) : x_{x}, y_{y} {}
double x_;
double y_;
};
using IComplex_DRectCoords = IComplex_Xfer<DRectCoords>;
template <>
struct IComplex_Impl<DRectCoords> {
static double xcoord(DRectCoords & self) { return self.x_; }
static double ycoord(DRectCoords & self) { return self.y_; }
static double argument(DRectCoords & self) { return std::atan(self.y_ / self.x_); }
static double magnitude(DRectCoords & self) {
double x = self.x_;
double y = self.y_;
return std::sqrt(x*x + y*y);
}
static void destruct_data(DRectCoords & self) { self.~DRectCoords(); }
};
// ----------------------------------------------------------------
// RComplex
//
// convenience router: supplies data argument to AComplex methods
// ----------------------------------------------------------------
template <typename Object>
struct RComplex : public Object {
using ObjectType = Object;
RComplex() {}
RComplex(Object::DataPtr data) : Object{std::move(data)} {}
int32_t _typeseq() const { return Object::iface()->_typeseq(); }
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 <typename Object>
bool
RComplex<Object>::_valid = valid_object_router<Object>();
namespace facet {
template <typename Object>
struct RoutingFor<AComplex, Object> {
using RoutingType = RComplex<Object>;
};
} /*namespace facet*/
namespace ut {
// ------ AComplex -----
// ----- TESTS -----
/** abstract interface for a complex number **/
struct AComplex {
using TypeErasedIface = struct IComplex_Any;
TEST_CASE("facet-1", "[facet]")
{
// AComplex passes abstract facet checks
REQUIRE(AComplex::_valid);
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;
// IComplex_Any passes facet implementation checks
REQUIRE(IComplex_Any::_valid);
}
virtual void destruct_data(void * data) const = 0;
TEST_CASE("xfer-polar-1", "[facet]")
{
IComplex_Impl<DPolarCoords> impl;
DPolarCoords z1{0.0, 1.0};
private:
static bool _valid;
};
REQUIRE(decltype(impl)::xcoord(z1) == 1.0);
REQUIRE(decltype(impl)::ycoord(z1) == 0.0);
REQUIRE(decltype(impl)::argument(z1) == 0.0);
REQUIRE(decltype(impl)::magnitude(z1) == 1.0);
}
bool
AComplex::_valid = valid_abstract_facet<AComplex>();
TEST_CASE("xfer-rect-1", "[facet]")
{
IComplex_Impl<DRectCoords> impl;
DRectCoords z1{1.0, 0.0};
// ----- IComplex_Any -----
REQUIRE(decltype(impl)::xcoord(z1) == 1.0);
REQUIRE(decltype(impl)::ycoord(z1) == 0.0);
REQUIRE(decltype(impl)::argument(z1) == 0.0);
REQUIRE(decltype(impl)::magnitude(z1) == 1.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; }
TEST_CASE("oobject-polar-1", "[facet]")
{
using Object = OObject<AComplex, DPolarCoords>;
virtual void destruct_data(void *) const final override { assert(false); }
DPolarCoords z1{0.0, 1.0};
Object obj(&z1);
private:
static bool _valid;
};
REQUIRE(obj.iface()->xcoord(obj.data()) == 1.0);
REQUIRE(obj.iface()->ycoord(obj.data()) == 0.0);
REQUIRE(obj.iface()->argument(obj.data()) == 0.0);
REQUIRE(obj.iface()->magnitude(obj.data()) == 1.0);
}
bool
IComplex_Any::_valid = true; //valid_interface_implementation<AComplex, IComplex_Any>();
TEST_CASE("oobject-rect-1", "[facet]")
{
using Object = OObject<AComplex, DRectCoords>;
DRectCoords z1{1.0, 0.0};
Object obj(&z1);
REQUIRE(obj.iface()->xcoord(obj.data()) == 1.0);
REQUIRE(obj.iface()->ycoord(obj.data()) == 0.0);
REQUIRE(obj.iface()->argument(obj.data()) == 0.0);
REQUIRE(obj.iface()->magnitude(obj.data()) == 1.0);
}
TEST_CASE("rrouter-polar-1", "[facet]")
{
using Router = RoutingType<AComplex, OObject<AComplex, DPolarCoords>>;
DPolarCoords z1{0.0, 1.0};
Router obj(&z1);
REQUIRE(obj.xcoord() == 1.0);
REQUIRE(obj.ycoord() == 0.0);
REQUIRE(obj.argument() == 0.0);
REQUIRE(obj.magnitude() == 1.0);
}
TEST_CASE("rrouter-rect-1", "[facet]")
{
using Router = RoutingType<AComplex, OObject<AComplex, DRectCoords>>;
DRectCoords z1{1.0, 0.0};
Router obj(&z1);
REQUIRE(obj.xcoord() == 1.0);
REQUIRE(obj.ycoord() == 0.0);
REQUIRE(obj.argument() == 0.0);
REQUIRE(obj.magnitude() == 1.0);
}
TEST_CASE("rrouter-any-1", "[facet]")
{
using Router = RoutingType<AComplex, OObject<AComplex>>;
// variant!
Router var1;
REQUIRE(var1.iface() != nullptr);
REQUIRE(var1.data() == nullptr);
{
DRectCoords z1{1.0, 0.0};
var1.from_data(&z1);
REQUIRE(var1.iface() != nullptr);
REQUIRE((void*)var1.data() == (void*)&z1);
REQUIRE(var1.xcoord() == z1.x_);
REQUIRE(var1.ycoord() == z1.y_);
REQUIRE(var1.xcoord() == 1.0);
REQUIRE(var1.ycoord() == 0.0);
REQUIRE(var1.argument() == 0.0);
REQUIRE(var1.magnitude() == 1.0);
REQUIRE(var1.downcast<DPolarCoords>() == nullptr);
REQUIRE(var1.downcast<DRectCoords>() == &z1);
}
{
DPolarCoords z2{0.0, 1.0};
var1.from_data(&z2);
REQUIRE(var1.iface() != nullptr);
REQUIRE((void*)var1.data() == (void*)&z2);
REQUIRE(var1.argument() == z2.arg_);
REQUIRE(var1.magnitude() == z2.mag_);
REQUIRE(var1.xcoord() == 1.0);
REQUIRE(var1.ycoord() == 0.0);
REQUIRE(var1.argument() == 0.0);
REQUIRE(var1.magnitude() == 1.0);
REQUIRE(var1.downcast<DRectCoords>() == nullptr);
REQUIRE(var1.downcast<DPolarCoords>() == &z2);
}
}
TEST_CASE("obj-rect-1", "[facet]")
{
DRectCoords z1{1.0, 0.0};
auto z1o = with_facet<AComplex>(&z1);
static_assert(std::is_same_v<decltype(z1o)::FacetType, AComplex>);
static_assert(std::is_same_v<decltype(z1o)::DataType, DRectCoords>);
REQUIRE(z1o._typeseq() == typeseq::id<DRectCoords>());
REQUIRE(z1o.xcoord() == 1.0);
REQUIRE(z1o.ycoord() == 0.0);
REQUIRE(z1o.argument() == 0.0);
REQUIRE(z1o.magnitude() == 1.0);
// downcast isn't part of interface for non-variant DRepr
// REQUIRE(z1o.downcast<DRectCoords>() == &z1);
double h = 0.5 * std::sqrt(2.0);
DRectCoords z2{h, h};
z1o.from_data(&z2);
REQUIRE(z1o.data() != &z1);
REQUIRE(z1o.data() == &z2);
REQUIRE(z1o.xcoord() == h);
REQUIRE(z1o.ycoord() == h);
REQUIRE(z1o.argument() == 0.25 * std::numbers::pi);
REQUIRE(z1o.magnitude() == 1.0);
*z1o = z1;
REQUIRE(z1o.data() == &z2);
REQUIRE(z1o.xcoord() == 1.0);
REQUIRE(z1o.ycoord() == 0.0);
REQUIRE(z1o.argument() == 0.0);
REQUIRE(z1o.magnitude() == 1.0);
}
TEST_CASE("obj-any-1", "[facet]")
{
obj<AComplex> var1;
REQUIRE(!var1);
REQUIRE(var1.iface() != nullptr);
REQUIRE(var1.data() == nullptr);
}
}
}