This repository is an experimental implementation of runtime polymorphism based on structural traits and type-erased trait handles.
#include "trp/shared_trait_ptr.hpp"
struct drawable {
void draw() const;
};
struct circle {
void draw() const {};
};
static_assert(trp::any_trait<drawable>);
static_assert(trp::implements_trait<circle, drawable const>);
void draw_object(trp::shared_trait_ptr<drawable const> ptr) {
ptr->draw();
}
int main() {
auto p = trp::make_shared_trait<drawable const, circle>();
draw_object(p);
}A TRP trait is a class type accepted by the trp::any_trait concept. A trait
must satisfy these restrictions:
- all members and base classes are public;
- no virtual functions or virtual bases;
- no non-static data members;
- no non-default special member functions;
- no operators;
- no non-static template methods;
- no rvalue-qualified methods or explicit-object-by-value methods;
- static data members are accepted only when recognized as
constexpr. The current check is GCC-specific. Clang effectively rejects static data members; - static functions are allowed only when they are templates used as default implementations for trait methods.
trp::implements_trait<Impl, Trait> is satisfied when every method in Trait
has a suitable implementation for Impl.
Implementation lookup uses this order:
-
trp::impl_spec_for<std::remove_cv_t<Impl>, std::remove_cv_t<Trait>>specialization;[!NOTE]
trp::impl_spec_for<I, T>requiresTto be cv-unqualified. -
trp::impl_spec_for<std::remove_cv_t<Impl>, TraitBase>for each base ofTrait;[!NOTE] This search is depth-first.
-
Direct public members with matching identifier:
- First direct public members of
Implthat do not satisfy all of are discarded:- the member is callable through a cv-qualified reference to
Implwith the arguments specified by the trait method; - the invocation is
noexceptif the trait method isnoexcept; - the return type matches type specified by the trait method:
- matches exactly if trait or method are annotated accordingly;
- convertible to if trait or method are annotated accordingly, default;
- the member is callable through a cv-qualified reference to
- trait method signature with potentially adjusted return type is checked. return type is adjusted if the method being checked returns a type diffreent from initial trait signature;
- if cv-promotion is allowed cv qualified signatures are checked
e.g. if return adjasted trait method signature is
int foo(int);and a member does not match it, thenint foo(int) constetc. are checked; - if parameter conversion is allowed an overload set is built and resolved using normal C++ overload resolution.
- First direct public members of
-
If there are no direct public members with matching identifier, steps 1-3 are repeated for public bases of
Impl, breadth-first. If there are multiple fitting candidates, the result is discarded. -
Explicit trait defaults
trp::default_impl_spec<std::remove_cv_t<Trait>>. -
Inline trait defaults i.e. static members with matching signature:
- method
auto foo(int) const -> int - inline default
static auto foo(const auto&, int) -> int
- method
The signature requirements are managed via annotations.
Annotating a trait only affects direct trait methods requirements.
Signature requirement annotations completely overwrite higher level annotations,
meaning method-level requriements overwrite global and trait signature requirements.
If a method is present in multiple supertraits, the requirements are combined to form a most strict form.
This means that a requirement cannot be lessened by redeclaration.
By default a relaxed signature is requied i.e. return type, argument and cv conversion is allowed.
Global level signature requirements can be controlled via macros:
#define TRP_DEFAULT_MATCH_METHOD_RETURN
#define TRP_DEFAULT_MATCH_METHOD_ARGS
#define TRP_DEFAULT_MATCH_METHOD_CV The library provides following annotations:
trp::relaxed_signature - default, least restrictive, requires method to be callable with convertible return type
trp::matching_return_signature - requires only exact return type
trp::matching_args_signature - requires only exact arguments, allows cv-promotion
trp::matching_cv_signature - requires exact arguments and cv-qualifications
trp::exact_signature - requires exact return and arguments, allows cv-promotion
trp::exact_cv_signature - requires exact return, arguments and cv-qualifications
Overload resolution limitations:
- Function templates cannot be inspected in C++ 26. There's no way to acquire instantiation from a call. The only reliable way to check function template is to check conversion to pointer to a member. Currently member function templates will only be selected if they match the signature exactly in the 3.2 or 3.3 or if they are the only candidate and the parameter conversion is allowed. If there is more than one candidate in 3.5 and one of them is a template the overload resolution cannot be done.
- Reflections of using-declaration is not present in C++26.
This means that if there are multiple bases providing
foomember, and the derived class uses using-declaration to disambiguate, the implementation overload resolution cannot be done.
The non-type erased handle is trp::trait_variant<Trait, Alternatives...>.
It is owning wrapper with value semantics.
Calling noexcept trait methods from a valueless variant calls std::terminate().
To prevent clashing with method object the following free functions are provided:
trp::index(Var const& var) -> tag_treturns index of the current alternativetrp::valueless_by_exception(Var const& var) -> boolreturns false if variant holds a valuetrp::emplace<I>(Var& var, Args&&... args)constructs an alternative I in placetrp::emplace<T>(Var& var, Args&&... args)constructs an alternative of type T in placetrp::get<I>(Variant&& var) -> decltype(auto)returns a reference to alternative Itrp::get<T>(Variant&& var) -> decltype(auto)returns a reference to alternative of type Tauto holds_alternative<T>(Var const& var)-> boolreturns true if current alternative is of type Ttrp::get_if<I>(Variant* var) -> decltype(auto)returns a pointer to alternative I, nullptr if not activetrp::get_if<T>(Variant* var) -> decltype(auto)returns a pointer to alternative of type T, nullptr if not active
The core the type-erased handle is trp::dyn_trait_ref<Trait>. It is a non-owning
view over a trait object.
Method access uses dot notation: ref.foo();. Because dot notation is reserved
for trait method access, operations on dyn_trait_ref itself are provided as
free functions:
trp::is_holding_type<Impl>(ref) -> boolchecks whether the implementation object isImpl.Implmust match cv-qualifiers exactly.trp::trait_cast<ExplicitSupertrait>(ref) -> dyn_trait_ref<ExplicitSupertrait>casts to an explicit supertrait. Any trait vtable includes all direct supertrait vtables, so this conversion is always valid.trp::trait_cast<AnotherTrait, Impl>(ref)returns a trait reference assuming the object isImpl. If the underlying object is not of typeImpl, calling any method is potentially undefined behavior.trp::is_valid_const_trait_cast<CVTrait>(ref) -> boolreturns true when the underlying object implements cv-qualifiedCVTrait.TraitandCVTraitmust have the same unqualified type.trp::const_trait_cast<CVTrait>(ref)returns a trait reference without checking validity. Calling any method after an invalid cast is potentially undefined behavior.
The repository also includes these owning handles:
trp::shared_trait_ptr, a reference-counted trait handle;trp::unique_trait_ptr, a new-allocated non-copyable trait handle;trp::alloc_unique_trait_ptr, an allocator-aware non-copyable trait handle.
The owning handles are currently basic. They share this API:
explicit operator bool()checks whether the handle stores an object;operator->andoperator*access the underlyingdyn_trait_ref;get() -> void*returns the type-erased implementation pointer;get<Impl>() -> Impl*returns the implementation pointer when the runtime type isImpl, otherwisenullptr;trp::is_holding_type<Impl>(ptr) -> boolchecks the implementation type;trp::trait_cast<ExplicitSupertrait>(ptr)moves or copies to an explicit supertrait handle;trp::trait_cast<AnotherTrait, Impl>(ptr)returns an empty owning handle unless the stored object isImpl.
Specific owning-handle APIs:
trp::make_shared_trait<Trait, Impl>(...)andtrp::allocate_shared_trait<Trait, Impl>(alloc, ...);trp::make_unique_trait<Trait, Impl>(...)andtrp::allocate_unique_trait<Trait, Impl>(alloc, ...);trp::unique_trait_ptr<Trait>can be moved intotrp::alloc_unique_trait_ptr<Trait>.
Currently, trp does not have a clear way to define a custom owning handle.
- Definition of traits
- Normal methods
- cv-qualified methods
-
noexceptqualification - Trait inheritance
- Default implementations, inline and explicit
- Explicit specialization of implementation methods
- Concepts:
any_trait: checks whether a type is valid as a trait definition;implements_trait: checks whether a type implements all methods of the trait;supertrait_of<S, T>: checks whether the method set ofSis a subset of the method set ofT;explicit_supertrait_of<S, T>:supertrait_of<S, T>andSis in the inheritance chain ofT; true forS == T;direct_supertrait_of<S, T>:supertrait_of<S, T>andSis a direct base class ofT; false forS == T.
-
trait_variant<T, Alternatives...> - Non-owning type-erased trait handle
dyn_trait_ref<T>-
dyn_trait_ref<cv_trait>wherecv_traitis cv-qualified - Upcasting to
explicit_supertrait_of<S, T>viatrait_cast<S> - Runtime type identification for implementations via
bool trp::is_holding_type<Impl>(const dyn_trait_ref<T>&) - Casting to other traits by providing the implementation type via
trait_cast<T, Impl>. This is a checked cast for owning handles and an unchecked cast fordyn_trait_ref. - (?) Conversion to non-explicit supertraits for allocator-aware handles by constructing vtables at runtime
-
- Basic owning trait handles
-
shared_trait_ptr,unique_trait_ptr, andalloc_unique_trait_ptr -
make_shared_trait,allocate_shared_trait,make_unique_trait, andallocate_unique_trait
-
C++26 reflection cannot generate types with methods. It can only generate aggregates with public data members.
The methods of a polymorphic trait object are emulated by data members of type
method_invoker<...> with the [[no_unique_address]] attribute and operator().
/* method-holder */ type is a standard-layout class with a first and only data member of type method_invoker<...>.
/* method-holder */ is defined via std::meta::define_aggregate to generate "methods" with the correct identifiers.
dyn_trait_ref_impl<...> is derived from all required holders and stores a vtable pointer and a type-erased object pointer.
/* dyn-ref-wrapper */ is derived from dyn_trait_ref_impl<...>.
To access the vtable and object pointers from inside operator(), the following
chain of casts is performed:
- The
thispointer ofmethod_invoker<...>::operator()(...)isreinterpret_castto a/* method-holder */pointer. This is well-defined because/* method-holder */is a standard-layout struct with a single member. - The
/* method-holder */pointer isstatic_castto a/* dyn-ref-wrapper */pointer. This is well-defined because of inheritance.
The vtable and type-erased object pointers are then acquired through the
/* dyn-ref-wrapper */ pointer.
Because access happens through a proxy, the cv-qualifiers of
dyn_trait_ref<Trait> must not be transient and should be inferred from
Trait. To achieve this, cvm_invoker<...> provides
operator()(auto const* vtable, void* obj, <method arguments>) with the correct
cv-qualifiers for each overload. A cv-qualified cvm_invoker<...> with
qualifiers equal to the Trait qualifiers is constructed and invoked.
This uses native C++ overload resolution for both argument types and cv resolution.
Variant uses similar technique, except it is cv-transient, and doesn't use type erasure and vtable.
Compilation is relatively slow. Some effort was made to minimize repeated evaluation in the implementation.
Some patterns in the trp implementation could be updated to more modern and
cleaner versions with additional C++26 features as compiler support matures.
Given the early stage of reflection compiler and tooling development, these issues may improve over time. However, current tooling challenges raise concerns about possible production usability.
Not implemented, but potentially feasible and interesting:
- Variant with type-erased extension. This can be viewed as speculatively devirtualized dynamic polymorphism.
- Definition of trait combinations, such as a greatest common supertrait or a common subtrait without inheritance and explicit definition
- Small object optimization
- (?) Non-type-erased reference wrapper to enforce restricted interfaces
At this moment, the repository is for experimenting and sharing.
- Configure with the p2996 preset:
TRP_P2996_INSTALL_PATH=/path/to/clang-p2996/install cmake --preset clang-p2996 - Configure with the GCC preset:
cmake --preset gcc - Build with the matching build preset:
cmake --build --preset <clang-p2996|gcc>