Parameters: reorganise type conversion dependencies

the generic cross-type assignment and -conversion will also be used by the `par::Scale` to join an automation evaluation result with a base value...
This commit is contained in:
Fischlurch 2026-09-17 22:00:26 +02:00
parent 6de07e9a36
commit c02d0a5a22
3 changed files with 20 additions and 216 deletions

View file

@ -35,11 +35,9 @@
#include "lib/par/spec.hpp"
#include "lib/meta/typelist.hpp"
#include "lib/meta/generator.hpp"
#include "lib/par/provision.hpp"
#include "lib/meta/generator.hpp"
#include "lib/meta/trait.hpp"
#include "lib/util.hpp"
namespace lib {

View file

@ -36,21 +36,13 @@
#include "lib/par/domain.hpp"
#include "lib/par/type-handler.hpp"
#include "lib/meta/generator.hpp"
#include <concepts>
#include <limits>
namespace lib {
namespace par {
using util::isNeg;
using std::same_as;
using std::floating_point;
using std::numeric_limits;
// GCC > 13 warns at class definition when a new overload shadows an inherited virtual function.
// While theoretically correct, in practice any call will be dispatched through the base interface,
// which in turn is also generated by metaprogramming from a type list.
@ -86,103 +78,10 @@ _Pragma("GCC diagnostic ignored \"-Woverloaded-virtual\"")
};
/* ===== type conversion details ===== */
/** Constraint: type \a SUB can not represent the full domain of type \a BAS
* @todo implicitly also implements a limitation to »numeric« types,
* due to the use of std::numeric_limits
*/
template<typename SUB, typename BAS>
concept sub_domain = std::signed_integral<SUB> != std::signed_integral<BAS>
or std::numeric_limits<SUB>::max() < std::numeric_limits<BAS>::max()
or std::numeric_limits<SUB>::lowest() > std::numeric_limits<BAS>::lowest()
;
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wsign-compare\"")
// the following functions are specifically crafted to work around
// the ambiguities caused by automatic signed-to-unsigned promotion;
// furthermore, the automatic promotion allows to perform the comparison.
template<typename X, typename Y>
inline constexpr bool
isLower_safe (X const& x, Y const& y)
{
return isNeg(x) == isNeg(y)? x < y
: isNeg(x);
}
template<typename X, typename Y>
inline constexpr bool
isLarger_safe (X const& x, Y const& y)
{
return isNeg(x) == isNeg(y)? x > y
: isNeg(y);
}
_Pragma("GCC diagnostic pop")
/** generic helper to conform into a common value domain
* @tparam SUB the implied target value domain
* @tparam BAS source data type
* @return a value in source data type,
* yet conformed into the target data range
*/
template<typename SUB, typename BAS>
inline constexpr BAS
preClamp (BAS const& value)
{
return value;
}
/** special case: clamp the source value into the target value domain.
* @remark tricky due to automatic conversions between signed/unsigned */
template<typename SUB, typename BAS> requires (sub_domain<SUB,BAS>
and not same_as<SUB,bool>)
inline constexpr BAS
preClamp (BAS const& rawVal)
{
static_assert (std::is_constructible_v<BAS,SUB const&>
,"Value domains must overlap at least partially "
"and values must be cross-constructible within overlap");
auto upperBound = numeric_limits<SUB>::max();
auto lowerBound = numeric_limits<SUB>::lowest();
return isLarger_safe(rawVal, upperBound)? BAS(upperBound)
: isLower_safe (rawVal, lowerBound)? BAS(lowerBound)
: rawVal;
}
/** special case: trigger a bool at the 0.5 level */
template<same_as<bool> B, floating_point F>
inline constexpr F
preClamp (F const& rawVal)
{
return 0.5 < rawVal? F{1} : F{0};
}
template<typename V, typename X>
inline void
assignConverted (V& targetVal, X const& srcVal)
{
if constexpr (std::is_assignable_v<V&, X const&>)
{
targetVal = preClamp<V> (srcVal);
}
else
if constexpr (std::is_constructible_v<V, X const&>)
{
targetVal.~U();
new(&targetVal) V (preClamp<V> (srcVal));
}
else
static_assert (!sizeof(X), "this type conversion is not supported");
}
/* ===== type conversion virtual dispatch ===== */
template<typename VAL, class IFA>
template<typename X, class PAR>

View file

@ -14,18 +14,12 @@
/** @file type-handler.hpp
** Generic building blocks to support parameter value conversions and adaptation.
** The implementation of parameters is built up in layers, starting from the generic
** interfaces \ref Provision, which describes the fundamental capabilities of a
** parameter as conceptualised here) and \ref Domain to support type conversions.
**
** All extensions beyond this point can be considered implementation-level services.
** The BaseDomain layer provides the actual type conversion services, based on internal
** knowledge of the concrete data type handled by this parameter.
**
** A \ref TypedEntity descriptor adds storage to capture type properties and allow
** for an ID registration. In addition, the concrete value type is tagged, marking
** the allowed value range, clipping behaviour and specifics of the scale (discrete,
** stepping, continuous, logarithmic...).
** The implementation of parameters is built such as to conceal the actual type of
** the parameter values -- which requires a virtual dispatch at some point. Yet the
** underlying type conversion functionality can be defined in a generic way, based
** on the actual types involved. Simple value-like parameters are based on a fixed
** collection of number types, both integral and floating-point. Thus the actual
** conversion is handled by the promotion- and coercion rules of C++ in most cases.
**
** @todo WIP-WIP-WIP can be considered an initial draft and placeholder, as of 2026
*/
@ -37,8 +31,10 @@
#include "lib/par/spec.hpp"
#include "lib/meta/trait.hpp"
#include "lib/par/domain.hpp"
#include "lib/meta/generator.hpp"
#include "lib/util.hpp"
#include <concepts>
#include <limits>
namespace lib {
@ -47,54 +43,20 @@ namespace par {
using util::isNeg;
using std::same_as;
using std::floating_point;
using std::signed_integral;
using std::numeric_limits;
// GCC > 13 warns at class definition when a new overload shadows an inherited virtual function.
// While theoretically correct, in practice any call will be dispatched through the base interface,
// which in turn is also generated by metaprogramming from a type list.
// See https://gcc.gnu.org/bugzilla/show_bug.cgi?id=109740
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Woverloaded-virtual\"")
template<typename VAL, class IFA>
struct DomainSetup
{
template<typename X, class PAR>
class TypeHandlerImpl
: public PAR
{
void extractAs (X& targetVal, ValBuff const& valBuff) const override;
void conform (ValBuff& targetBuff, X const& srcVal) override;
};
using TypeHandlerChain = meta::InstantiateChained<BaseTypes::List, TypeHandlerImpl, IFA>;
};
template<typename VAL, class IFA>
class BaseDomain
: public DomainSetup<VAL, IFA>::TypeHandlerChain
{
public:
void applyLimit (ValBuff&) override;
void transferTo (ValBuff const& source, Domain& targetDomain, ValBuff& target) const override;
};
/* ===== type conversion details ===== */
/* ===== type conversion implementation details ===== */
/** Constraint: type \a SUB can not represent the full domain of type \a BAS
* @todo implicitly also implements a limitation to »numeric« types,
* due to the use of std::numeric_limits
*/
template<typename SUB, typename BAS>
concept sub_domain = std::signed_integral<SUB> != std::signed_integral<BAS>
or std::numeric_limits<SUB>::max() < std::numeric_limits<BAS>::max()
or std::numeric_limits<SUB>::lowest() > std::numeric_limits<BAS>::lowest()
concept sub_domain = signed_integral<SUB> != signed_integral<BAS>
or numeric_limits<SUB>::max() < numeric_limits<BAS>::max()
or numeric_limits<SUB>::lowest() > numeric_limits<BAS>::lowest()
;
@ -122,6 +84,7 @@ _Pragma("GCC diagnostic ignored \"-Wsign-compare\"")
_Pragma("GCC diagnostic pop")
/** generic helper to conform into a common value domain
* @tparam SUB the implied target value domain
* @tparam BAS source data type
@ -175,7 +138,7 @@ _Pragma("GCC diagnostic pop")
else
if constexpr (std::is_constructible_v<V, X const&>)
{
targetVal.~U();
targetVal.~V();
new(&targetVal) V (preClamp<V> (srcVal));
}
else
@ -183,62 +146,6 @@ _Pragma("GCC diagnostic pop")
}
template<typename VAL, class IFA>
template<typename X, class PAR>
inline void
DomainSetup<VAL,IFA>::TypeHandlerImpl<X,PAR>::extractAs (X& targetVal, ValBuff const& valBuff) const
{
VAL const& srcVal = asValue<VAL> (valBuff);
assignConverted (targetVal, srcVal);
}
template<typename VAL, class IFA>
template<typename X, class PAR>
inline void
DomainSetup<VAL,IFA>::TypeHandlerImpl<X,PAR>::conform (ValBuff& targetBuff, X const& srcVal)
{
VAL& targetVal = asValue<VAL> (targetBuff);
assignConverted (targetVal, srcVal);
}
template<typename VAL, class IFA>
inline void
BaseDomain<VAL,IFA>::applyLimit (ValBuff& valBuff)
{
VAL& val{asValue<VAL> (valBuff)};
val = preClamp<VAL> (val);
}
template<typename VAL, class IFA>
inline void
BaseDomain<VAL,IFA>::transferTo (ValBuff const& source, Domain& targetDomain, ValBuff& target) const
{
targetDomain.conform (target, asValue<VAL> (source));
targetDomain.applyLimit (target);
}
/**
* Service-Interface: represent a specific configuration of parameter value type,
* range and scale. Every ParamType descriptor is also a _prototype_ and has
* an unique identity, ensured by registration, so that it acts as a type identifier.
* @todo RLY? registration??
* Furthermore, the interface exposes allowed conversion and conforming operations.
*/
template<typename VAL, class IFA>
class TypedEntity
: public BaseDomain<VAL,IFA>
{
public:
};
}} // namespace lib::par
#endif /*LIB_PAR_TYPE_HANDLER_H*/