diff --git a/research/try.cpp b/research/try.cpp index 6fa4a24b9..972451b90 100644 --- a/research/try.cpp +++ b/research/try.cpp @@ -1,6 +1,7 @@ /* try.cpp - to try out and experiment with new features.... * scons will create the binary bin/try */ +// 03/26 - runtime index access and iteration over tuple-likes // 06/25 - provide a concept to accept _tuple-like_ objects // 06/25 - investigate function type detection of std::bind Binders // 12/24 - investigate problem when perfect-forwarding into a binder @@ -10,22 +11,28 @@ /** @file try.cpp - * Develop a concept to detect _tuple-like_ classes, based on the requirements - * of the »tuple protocol«. Using some ideas from [Stackoverflow] as starting point. - * However, we model both a _friend function_ `get` and a similar member function - * as alternatives for element access, and we break down the checks into sub-concepts. - * [Stackoverflow]: https://stackoverflow.com/q/68443804/444796 + * Work out a construction scheme to build an efficient element-accessor to data + * in a tuple-like with run-time indexing. As an extension, run-time pulled iteration + * can be established, using the IterExplorer framework. + * @see lib::meta::FunTrampoline + * @see lib::meta::TupleIdxAdaptor */ #include "lib/format-cout.hpp" +#include "lib/format-util.hpp" #include "test/test-helper.hpp" #include "test/diagnostic-output.hpp" +#include "lib/meta/common-result.hpp" +#include "lib/meta/tuple-concept.hpp" #include "lib/meta/tuple-helper.hpp" +#include "lib/meta/function.hpp" +#include "lib/iter-adapter.hpp" #include "lib/hetero-data.hpp" +#include "lib/meta/trait.hpp" #include "lib/util.hpp" -#include +#include using std::string; @@ -41,7 +48,7 @@ template void show() { - cout << "Tup!! "<< ::test::showType() <() < ([](auto i) { using Elm = std::tuple_element_t; @@ -49,6 +56,255 @@ show() }); } +namespace lib { +namespace meta{ + +#if false ///////////////////////////////////////////////////////////////////////////////////////////////////TODO : new generic library functions + /** + * Concept to require a generic type sequence `Types` + */ + template + concept typeseq = requires (TSEQ seq) + { + // TSEQ can invoke a function with a Types subclass + [](Types&){/*placeholder*/}(seq); + }; +#endif ///////////////////////////////////////////////////////////////////////////////////////////////////(End) : new generic library functions + + + template class BUILD, typeseq TSEQ> + class FunTrampoline + { + using AllElm = ElmTypes; + using Indices = AllElm::Idx; + using Builder = BUILD; + using Functor = Builder::Functor; + using Table = std::array; + + template + static constexpr Table + buildFunctors (std::index_sequence) + { + return {Builder().template build()...}; + } + + static constexpr Table functors = buildFunctors (Indices{}); + + public: + static constexpr size_t size() { return functors.size(); } + static constexpr size_t memSiz() { return sizeof(functors); } + + static Functor const& + dispatch (size_t idx) + { + ENSURE (idx < functors.size()); + return functors[idx]; + } + + using ResType = _Fun::Ret; + }; + + +#if false ///////////////////////////////////////////////////////////////////////////////////////////////////TODO : new generic library functions + template + using isConst = std::is_const>; + template + using isLRef = std::is_lvalue_reference; + + + template + using Any_Const = ElmTypes::template OrAll; + + template + using All_LRef = ElmTypes::template AndAll; + + template + static constexpr bool Any_Const_v = Any_Const::value; + + template + static constexpr bool All_LRef_v = All_LRef::value; + + + + + template + struct ProbeCommonType + { + + template + using Reconciled = decltype(true ? std::declval() : std::declval()); + + + template + struct FoundResult + : std::true_type + { + using Type = RES; + }; + struct NoResultFound + : std::false_type + { }; + + template> + static FoundResult> + check(int); + + template + static NoResultFound + check(...); + + using FirstResult = decltype(check(0)); + + template + struct Join + { + using Result = FirstResult; + }; + + template + struct Join + { + //wenn RES erfolgreich + using CT = FirstResult::Type; + using Result = ProbeCommonType::Result; + }; + + using Result = Join::Result; + }; + + + + template + struct CommonResult + : ProbeCommonType::Result + { }; + + template + struct CommonResult + : CommonResult + { }; + + template<> + struct CommonResult<> + : std::false_type + { }; + + template + struct CommonResult> + : CommonResult + { }; + + template + using CommonResult_t = CommonResult::Type; + + template + static constexpr bool has_CommonResult_v = CommonResult::value; + + + template + concept reconcilable = requires + { + typename CommonResult::Type; + }; + + template + concept reconcilable_ref = reconcilable + and isLRef_v>; + + template + struct CommonResultTraits + { + static_assert (reconcilable, "unable to reconcile these types"); + + using CommonVal = std::common_type_t; + using ResType = CommonResult_t; + //NOTE: std::common_type decays (strips CV and ref) + static constexpr bool makeConst = isConst_v; + static constexpr bool exposeRef = isLRef_v; + }; +#endif ///////////////////////////////////////////////////////////////////////////////////////////////////(End) : new generic library functions + + + + template + class TupleIndexAdaptor + : public TUP + { + template + struct SubscriptBuilder + { + using Tuple = RefTraits::value_type; + using AddRefs = ElmTypes::template Apply; + using ResType = CommonResult_t; + using Functor = ResType(*)(Tuple&); + + template + constexpr auto + build() + { + return [](Tuple& tuple) -> ResType + { + return getElm (tuple); + }; + } + }; + + using TypeSeq = ElmTypes::Seq; + using Accessor = FunTrampoline; + public: + using ResType = Accessor::ResType; + using reference = ResType; + using value_Type = RefTraits::value_type; + + template + TupleIndexAdaptor(T&& dataSrc) + : TUP{forward (dataSrc)} + { } + + using TUP::TUP; + + static constexpr size_t size() { return Accessor::size(); } + + ResType + operator[] (size_t idx) + { + return Accessor::dispatch(idx)(*this); + } + }; + + template + TupleIndexAdaptor(TUP) -> TupleIndexAdaptor>; +}} + +using namespace lib::meta; + +template +struct Stringify + { + using Tuple = ElmTypes::Tup; + using Functor = string(*)(Tuple const&); + + template + constexpr auto + build() + { + using ElmT = std::tuple_element_t; + + return [](Tuple const& tuple) + { + return util::toString (getElm (tuple)); + }; + } + + }; + + +struct Uber { }; +struct Untr : Uber { }; +struct Unta : Uber { }; + int main (int, char**) @@ -57,20 +313,10 @@ main (int, char**) using Arr = std::array; using Hetero = lib::HeteroData::Chain::ChainExtent::ChainType; - SHOW_EXPR((lib::meta::tuple_sized )) - SHOW_EXPR((lib::meta::tuple_sized )) - SHOW_EXPR((lib::meta::tuple_sized )) - SHOW_EXPR((lib::meta::tuple_sized )) - - SHOW_EXPR((lib::meta::tuple_element_accessible)) -// SHOW_EXPR((lib::meta::tuple_element_accessible)) - SHOW_EXPR((lib::meta::tuple_element_accessible)) - SHOW_EXPR((lib::meta::tuple_accessible)) - SHOW_EXPR((lib::meta::tuple_accessible)) - SHOW_EXPR((lib::meta::tuple_like )) SHOW_EXPR((lib::meta::tuple_like )) SHOW_EXPR((lib::meta::tuple_like )) + SHOW_EXPR((lib::meta::tuple_like )) SHOW_EXPR((lib::meta::tuple_like )) show(); @@ -78,25 +324,112 @@ main (int, char**) show(); show(); - SHOW_EXPR((std::tuple_size_v)) - using Elm1 = std::tuple_element_t<1, const Tup>; - SHOW_TYPE(Elm1) - using TupConstSeq = lib::meta::ElmTypes::Seq; - SHOW_TYPE(TupConstSeq) + using TupStringy = FunTrampoline::Seq>; +SHOW_EXPR(TupStringy::size()) - using T1 = decltype(lib::meta::getElm<0> (std::declval())); - SHOW_TYPE(T1) - using T2 = decltype(lib::meta::getElm<0> (std::declval())); - SHOW_TYPE(T2) - using T3 = decltype(lib::meta::getElm<0> (std::declval())); - SHOW_TYPE(T3) - - using H1 = decltype(lib::meta::getElm<4> (std::declval())); - SHOW_TYPE(H1) - using H2 = decltype(lib::meta::getElm<4> (std::declval())); - SHOW_TYPE(H2) - using H3 = decltype(lib::meta::getElm<4> (std::declval())); - SHOW_TYPE(H3) + Tup tup23{2,3}; + Tup tup42{4,2}; +SHOW_TYPE(decltype(std::get<1>(tup42))) +SHOW_EXPR(std::get<1>(tup42)) +SHOW_EXPR(TupStringy::dispatch(0)(tup23)) +SHOW_EXPR(TupStringy::dispatch(0)(tup42)) +SHOW_EXPR(TupStringy::dispatch(1)(tup23)) +SHOW_EXPR(TupStringy::dispatch(1)(tup42)) + + using T1 = Types; + using T2 = Types; + using T3 = Types; +SHOW_EXPR((has_CommonResult_v)) +SHOW_EXPR((has_CommonResult_v)) +SHOW_EXPR((has_CommonResult_v)) +SHOW_EXPR((has_CommonResult_v<>)) +SHOW_EXPR((has_CommonResult_v)) +SHOW_EXPR((has_CommonResult_v)) + +SHOW_TYPE(CommonResult_t) +SHOW_TYPE(CommonResult_t) +SHOW_TYPE(CommonResult_t) +using CoTx = CommonResult_t; +SHOW_TYPE(CoTx) + + + TupleIndexAdaptor idxAdapt{tup42}; +SHOW_EXPR(sizeof(tup42)) +SHOW_EXPR(sizeof(idxAdapt)) +SHOW_TYPE(TupleIndexAdaptor::ResType) +SHOW_EXPR(idxAdapt.size()) +SHOW_EXPR(idxAdapt[0]) +SHOW_EXPR(idxAdapt[1]) +SHOW_EXPR(getElm<0>(static_cast (idxAdapt))) +SHOW_EXPR(getElm<1>(static_cast (idxAdapt))) +SHOW_TYPE(decltype(idxAdapt[1])) + + using Tup2 = std::tuple; + int ii{23}; + Tup2 tup21{-1,ii,55}; + TupleIndexAdaptor idxApt2{tup21}; +SHOW_TYPE(decltype(tup21)) + using IdxApt2 = decltype(idxApt2); +SHOW_TYPE(IdxApt2) +SHOW_TYPE(IdxApt2::ResType) +SHOW_EXPR(idxApt2[0]) +SHOW_EXPR(idxApt2[1]) +SHOW_EXPR(idxApt2[2]) +SHOW_EXPR(++idxApt2[0]) +SHOW_EXPR(idxApt2[1]++) +SHOW_EXPR(++++idxApt2[2]) +SHOW_EXPR(tup21) +SHOW_EXPR(Tup2(idxApt2)) +SHOW_EXPR(ii) + + using TupIt = lib::IterableDecorator>; + TupIt tupIt{move(idxApt2)}; +SHOW_EXPR( bool(tupIt) ) +SHOW_EXPR( *tupIt ) +SHOW_TYPE(decltype(*tupIt)) + ++*tupIt; + --ii; +SHOW_EXPR( *tupIt ) +SHOW_EXPR(util::join(tupIt, "▶▶")) + +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable_ref )) +SHOW_EXPR(( reconcilable_ref )) +SHOW_EXPR(( reconcilable_ref )) +SHOW_EXPR(( reconcilable_ref )) + using TX = CommonResult_t; +SHOW_TYPE(TX) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable )) + using TX2 = CommonResult_t; +SHOW_TYPE(TX2) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable_ref )) +SHOW_EXPR(( reconcilable_ref )) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable )) + using TX3 = CommonResult_t; +SHOW_TYPE(TX3) + using TX4 = CommonResult_t; +SHOW_TYPE(TX4) +SHOW_EXPR(( reconcilable )) + using TX5 = CommonResult_t; +SHOW_TYPE(TX5) + using TX6 = CommonResult_t; +SHOW_TYPE(TX6) +SHOW_EXPR(( CommonResult::value )) +SHOW_EXPR(( CommonResult::value )) +SHOW_EXPR(( CommonResult::value )) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable )) +SHOW_EXPR(( reconcilable )) cout << "\n.gulp." < + struct TypeDiagnostics + { + using Type = X; + static constexpr auto prefix = ""; + static constexpr auto postfix = ""; + }; + template + struct TypeDiagnostics + { + using Type = X; + static constexpr auto prefix = "const "; + static constexpr auto postfix = ""; + }; + template + struct TypeDiagnostics + { + using Type = X; + static constexpr auto prefix = ""; + static constexpr auto postfix = "&"; + }; + template + struct TypeDiagnostics + { + using Type = X; + static constexpr auto prefix = ""; + static constexpr auto postfix = " &&"; + }; + template + struct TypeDiagnostics + { + using Type = X; + static constexpr auto prefix = ""; + static constexpr auto postfix = " const&"; + }; + template + struct TypeDiagnostics + { + using Type = X; + static constexpr auto prefix = "const "; + static constexpr auto postfix = " &&"; + }; + template + struct TypeDiagnostics + { + using Type = X; + static constexpr auto prefix = ""; + static constexpr auto postfix = " *"; + }; + template + struct TypeDiagnostics + { + using Type = X; + static constexpr auto prefix = "const "; + static constexpr auto postfix = " *"; + }; + template + struct TypeDiagnostics + { + using Type = X; + static constexpr auto prefix = "const "; + static constexpr auto postfix = " * const"; + }; + template + struct TypeDiagnostics + { + using Type = X; + static constexpr auto prefix = ""; + static constexpr auto postfix = " * const"; + }; + template + struct TypeDiagnostics + { + using Type = X; + static constexpr auto prefix = ""; + static constexpr auto postfix = " * const *"; + }; + }//(End)decoration helper + + /** diagnostic type output, including const and similar adornments + * @warning operates after-the-fact and relies on mangled type names + * plus several heuristics. Output might thus not be entirely correct, + * especially when several levels of const, pointer and references are involved. + * If in doubt, place the test::TypeDebugger to reveal the type as the compiler sees it. + * @remarks the function lib::meta::typeStr was deliberately written such as to remove type adornments + * and does not work on all kinds of reference. This helper complements this and + * attempts to preserve const-ness, pointer and reference decoration. + */ + template + inline std::string + showType() + { + using Case = TypeDiagnostics; + using Type = Case::Type; + + return Case::prefix + + lib::meta::humanReadableTypeID (typeid(Type).name()) + + Case::postfix; + } + + template + inline std::string + showTypes() + { + return "<| " + ((showType()+", ") + ... + "|>"); + } + + + + namespace { /** toggle to prefer specialisation with direct lexical conversion */ template diff --git a/src/lib/iter-adapter.hpp b/src/lib/iter-adapter.hpp index e375802fb..a7f85a150 100644 --- a/src/lib/iter-adapter.hpp +++ b/src/lib/iter-adapter.hpp @@ -761,7 +761,11 @@ namespace lib { , i_{0} { } - IdxStoreCore() = default; + IdxStoreCore() + : CON{} + , i_{CON::size()} + { } // explicitly ensure that iterator is empty + // copy and assignment acceptable (warning!) @@ -775,8 +779,8 @@ namespace lib { decltype(auto) yield() const { - return CON::operator[] (i_); - } + return util::unConst(*this).operator[] (i_); + } // rationale: const iterator ≠ const_iterator void iterNext() diff --git a/src/lib/iter-explorer.hpp b/src/lib/iter-explorer.hpp index a220efb98..ea2349199 100644 --- a/src/lib/iter-explorer.hpp +++ b/src/lib/iter-explorer.hpp @@ -102,6 +102,7 @@ #include "lib/error.hpp" #include "lib/uninitialised-storage.hpp" #include "lib/meta/duck-detector.hpp" +#include "lib/meta/common-result.hpp" #include "lib/meta/function.hpp" #include "lib/meta/trait.hpp" #include "lib/item-wrapper.hpp" @@ -305,7 +306,7 @@ namespace lib { template struct _DecoratorTraits>> { - static_assert (not std::is_rvalue_reference::value, + static_assert (not std::is_rvalue_reference_v, "container needs to exist elsewhere during the lifetime of the iteration"); using SrcIter = iter_explorer::StlRange; using SrcVal = SrcIter::value_type; @@ -416,19 +417,15 @@ namespace lib { using ResIter = _DecoratorTraits::SrcIter; using SrcYield = iter::Yield; using ResYield = iter::Yield; - using _CommonT = meta::CommonResultYield; + using _CommonT = meta::CommonResult; static constexpr bool can_reconcile = _CommonT::value; - static constexpr bool isRefResult = _CommonT::isRef; static_assert (can_reconcile, "source iterator and result from the expansion must yield compatible values"); static_assert (is_const_v == is_const_v, "source and expanded types differ in const-ness"); - using YieldRes = _CommonT::ResType; - using value_type = _CommonT::value_type; - using reference = _CommonT::reference; - using pointer = _CommonT::pointer; + using YieldRes = _CommonT::Type; }; }//(End) IterExplorer traits @@ -749,9 +746,9 @@ namespace lib { /** @note result type bindings based on a common type of source and expanded result */ using YieldRes = _Trait::YieldRes; - using value_type = _Trait::value_type; - using reference = _Trait::reference; - using pointer = _Trait::pointer; + using value_type = meta::RefTraits::value_type; + using reference = meta::RefTraits::reference; + using pointer = meta::RefTraits::pointer; bool diff --git a/src/lib/meta/common-result.hpp b/src/lib/meta/common-result.hpp new file mode 100644 index 000000000..318a240a0 --- /dev/null +++ b/src/lib/meta/common-result.hpp @@ -0,0 +1,236 @@ +/* + COMMON-RESULT.hpp - reconciled return types + + Copyright (C) + 2024,2026 Hermann Vosseler + +  **Lumiera** is free software; you can redistribute it and/or modify it +  under the terms of the GNU General Public License as published by the +  Free Software Foundation; either version 2 of the License, or (at your +  option) any later version. See the file COPYING for further details. + +*/ + +/** @file common-result.hpp + ** Determine a common result type for combined functors. + ** This header defines a metafunction to determine if, and how the return types + ** of several functors or accessors can be combined. This task becomes relevant + ** when building exploration strategies defined over a functional datastructure. + ** A notable example happens when performing a _tree expansion_ from within a + ** [iterator pipeline](\ref iter-explorer.hpp) or when mapping an aggregating + ** functor onto a [tuple of values](\ref tuple-concept.hpp). + ** + ** The basic technique to find such a _reconciled type_ is to rely on the + ** _ternary operator_ (`test? yes_expr : no_expr;`), which is specified to + ** great detail in the C++ standard. However, the STDLIB provides only two + ** tools tailored for specific cases: + ** - std::common_type is what you can use to _store_ such a reconciled result, + ** and includes _decaying_ the type. + ** - std::common_reference is oriented towards passing of references, + ** and includes elaborate mapping logic for mixture of RValues and LValues. + ** + ** So there is a need to expose the foundational logic for type reconciliation, + ** without any special adaptation rules. Furthermore, direct support for passing + ** the selection of types in the form of a [Type Sequence](\ref typelist.hpp) + ** is included. + ** + ** @see CommonResult_test + ** @see lib::iter_explorer::Expander + ** @see tuple-idx-adaptor.hpp + */ + + +#ifndef LIB_META_COMMON_RESULT_H +#define LIB_META_COMMON_RESULT_H + + +#include "lib/meta/trait.hpp" +#include "lib/meta/typelist.hpp" + + + +namespace lib { +namespace meta { + + namespace detail {// Implementation: reconcile N return types... + + /** @internal helper to find a reconciled result type. + * This is the technical part, and relies on a SFINAE technique: + * The test succeeds, if a ternary operator with both type results + * can be instantiated in the signature of a `check` template. + * + * Furthermore, the reconciliation for a pair of types can be + * generalised to N types by gradually reconciling the following + * types with the already established result. + * + * @remark 2026 the GNU Libstdc++ relies on precisely the same + * technique, which implies that this approach works reliably. + */ + template + class _ProbeCommonType + { + /******************************************************************************//** + * Core of the type reconciliation logic: determine the _common type_. + * The implementation relies on the **ternary operator** — the C++ language + * specifies a detailed and elaborate algorithm for this operator to reconcile + * the results of the "true" and "false" parts of `test? true_part : false_part`; + * such an expression compiles if and only if these criteria are met and a common + * type can be determined. The results of this procedure are quite solid, and + * satisfactory in most usage situations. A mixture of references, values const + * and volatile is handled properly. Only some rare corner cases are not covered, + * like upcasting two subclasses to a common interface (actually in some cases, + * involving multiple inheritance, a single authoritative solution for this + * special problem does not exist, and thus these cases were left out + * from the language spec). + */ + template + using Reconciled = decltype(true ? std::declval() : std::declval()); + + + /** Marker type: successful test */ + template + struct FoundResult + : std::true_type + { + using Type = RES; + }; + struct NoResultFound + : std::false_type + { }; + + + /** SFINAE setup to probe for a reconciled type */ + template> + static FoundResult> + check(int); + + template + static NoResultFound + check(...); + + /** perform the reconciliation test */ + using FirstResult = decltype(check(42)); + + + /** Helper: Join results of pairwise reconciliation + * to extend the result to a sequence of types */ + template + struct Join + { + using Result = FirstResult; + }; + + template + struct Join ///<@note matches when FirstResult was successful + { + using CT = FirstResult::Type; + using Result = _ProbeCommonType::Result; + }; + + + public: + /** Retrieve the overall result of this metafunction */ + using Result = Join::Result; + }; + + }//(End)implementation: type reconciliation + + + + /** + * Metafunction to find a _common result type_. + * @tparam TYPES a variadic sequence of types to be reconciled. + * Finding such a common result type can be relevant when results + * from several functors shall be combined, e.g. when mapping and + * aggregating results of a function application to a tuple. + * It is important to note that such a function may return arbitrary + * types, either by value or by reference, and possibly marked as const. + * + * \par Interface + * The `struct` CommonResult itself is a std::bool_constant that + * indicates if the given types can be reconciled. When successful, + * a nested typedef `Type` is present and aliased to a type that + * can represent all given result types as a function return. + * + * @remarks + * - the C++ standard offers similar functionality as + * + std::common_type + * + std::common_reference + * - however, the standard solution, while based on the same logic + * as this implementation, adds additional transformations, like + * decaying the type (for std::common_type) or handling various + * corner cases with RValue references. The specification of + * expected behaviour is quite elaborate for those cases + * - the implementation presented here is focused on _returning_ + * results from a function and thus relies _solely_ on the + * logic of the ternary operator (`? : `) + * - when several LValue-reference types are given, the result + * is only a reference when all results can be presented safely + * through such a reference; in most cases the result falls back + * to returning by-value. + * @note Concepts are provided to require that a selection of types + * is reconcilable, or even reconcilable into a reference type. + * @see CommonResult_test + */ + template + struct CommonResult + : detail::_ProbeCommonType::Result + { }; + + template + struct CommonResult + : CommonResult + { }; + + template<> + struct CommonResult<> + : std::false_type + { }; + + /** Specialisation to work transparently also on a type sequence, + * passed-in as a single template parameter. */ + template + struct CommonResult> + : CommonResult + { }; + + + + template + using CommonResult_t = CommonResult::Type; + + template + static constexpr bool has_CommonResult_v = CommonResult::value; + + + + template + concept reconcilable = requires + { + typename CommonResult::Type; + }; + + template + concept reconcilable_ref = reconcilable + and isLRef_v>; + + + + template + struct CommonResultTraits + { + static_assert (reconcilable, "unable to reconcile these types"); + + using CommonVal = std::common_type_t; + using ResType = CommonResult_t; + //NOTE: std::common_type decays (strips CV and ref) + static constexpr bool makeConst = isConst_v; + static constexpr bool exposeRef = isLRef_v; + }; + + + +}} // namespace lib::meta +#endif /*LIB_META_COMMON_RESULT_H*/ diff --git a/src/lib/meta/fun-trampoline.hpp b/src/lib/meta/fun-trampoline.hpp new file mode 100644 index 000000000..7a7948426 --- /dev/null +++ b/src/lib/meta/fun-trampoline.hpp @@ -0,0 +1,125 @@ +/* + FUN-TRAMPOLINE.hpp - generate static functor table + + Copyright (C) + 2026 Hermann Vosseler + +  **Lumiera** is free software; you can redistribute it and/or modify it +  under the terms of the GNU General Public License as published by the +  Free Software Foundation; either version 2 of the License, or (at your +  option) any later version. See the file COPYING for further details. + +*/ + +/** @file fun-trampoline.hpp + ** Build a trampoline table with functors. + ** This Metaprogramming helper uses a _builder template_ to fill an embedded + ** static dispatcher table with generated functors. These can then be accessed + ** by index, typically to be invoked. + ** + ** The setup works entirely on a static level, so that any mention of some + ** FunTrampoline template instantiation causes the compiler to populate the + ** corresponding functor table. This arrangement can be used, for example, + ** to auto-generate an index accessor for a specific tuple, so that a + ** runtime subscript or iteration can be derived. Obviously doing so + ** requires the types in the tuple to be [reconcilable](\ref common-type.hpp), + ** so that a generic functor can be defined to handle all fields in the tuple + ** based on a common generic scheme. Without that, tuples can in general + ** only be iterated and transformed by compile-time metaprogramming. + ** + ** @see FunTrampoline_test + ** @see CommonResult_test + ** @see tuple-idx-adaptor.hpp + */ + + +#ifndef LIB_META_FUN_TRAMPOLINE_H +#define LIB_META_FUN_TRAMPOLINE_H + + +#include "include/logging.h" +#include "lib/meta/function.hpp" +#include "lib/meta/variadic-helper.hpp" + +#include +#include + + +namespace lib { +namespace meta { + + /** + * FunTrampoline relies on a helper template + * to be used as _trampoline builder_ to generate + * a sequence of functors that can be placed into a + * static dispatcher table. Thus, a trampoline_builder + * - must be default constructible + * - must define a Functor type... + * - that can be used as array element + * - and must provide a templated build() function + */ + template + concept trampoline_builder = requires(B builder) + { + typename B::Functor; + std::array{}; + requires std::default_initializable; + { builder.template build<0>() } -> std::convertible_to; + }; + + + /** + * A static functor table, populated automatically. + * @tparam BUILD helper Template to generate functors + * that are placed into a static table + * @tparam TSEQ sequence of types to instantiate + * the functors with, iteratively. + * @remarks + * - the given type sequence is handled with the + * ElmTypes helper template (see \ref variadic-helper.hpp) + * - the BUILD template is instantiated with that \a TSEQ + * - statically-for-each-index, the build() function + * on the builder is invoked, which is expected to produce + * an instance of BUILD::Functor + * @warning as soon as some instance of this FunTrampoline + * is used / referred, the compiler will emit the trampoline + * table into static storage; the resulting increase of the + * executable size is something to be aware of, when using + * this construct for transient processing, driven by + * actual argument types. + */ + template class BUILD, typeseq TSEQ> + requires trampoline_builder> + class FunTrampoline + { + using AllElm = ElmTypes; + using Indices = AllElm::Idx; + using Builder = BUILD; + using Functor = Builder::Functor; + using Table = std::array; + + template + static constexpr Table + buildFunctors (std::index_sequence) + { + return {Builder().template build()...}; + } + + static constexpr Table functors = buildFunctors (Indices{}); + + public: + static constexpr size_t size() { return functors.size(); } + static constexpr size_t memSiz() { return sizeof(functors);} + + static Functor const& + dispatch (size_t idx) + { + ENSURE (idx < functors.size()); + return functors[idx]; + } + + using ResType = _Fun::Ret; + }; + +}}// namespace lib::meta +#endif /*LIB_META_FUN_TRAMPOLINE_H*/ diff --git a/src/lib/meta/trait.hpp b/src/lib/meta/trait.hpp index 930767930..9f2be1927 100644 --- a/src/lib/meta/trait.hpp +++ b/src/lib/meta/trait.hpp @@ -103,13 +103,22 @@ namespace meta { using std::__or_; template - static constexpr bool isConst_v = std::is_const_v>; + using isConst = std::is_const>; template - static constexpr bool isLRef_v = std::is_lvalue_reference_v; + using isLRef = std::is_lvalue_reference; template - static constexpr bool isRRef_v = std::is_rvalue_reference_v; + using isRRef = std::is_rvalue_reference; template - static constexpr bool isRef_v = std::is_reference_v; + using isRef = std::is_reference; + + template + static constexpr bool isConst_v = isConst::value; + template + static constexpr bool isLRef_v = isLRef::value; + template + static constexpr bool isRRef_v = isRRef::value; + template + static constexpr bool isRef_v = isRef::value; /** diff --git a/src/lib/meta/tuple-concept.hpp b/src/lib/meta/tuple-concept.hpp index d7de979fd..207e07b2f 100644 --- a/src/lib/meta/tuple-concept.hpp +++ b/src/lib/meta/tuple-concept.hpp @@ -105,13 +105,35 @@ namespace meta { and tuple_accessible>; + + /** + * Concept to require an instance of std::tuple` + * @note other than `concept` \ref tuple_like, this aims + * explicitly at the implementation form STDLIB. + */ + template + concept tup = requires (TUP tup) + { + // check that a function can be invoked with a std::tuple + [](std::tuple){/*placeholder*/}(tup); + }; + + template + struct is_Tuple : std::bool_constant> { }; + + template + static constexpr bool is_Tuple_v = is_Tuple::value; + + + + /** * Helper for abstracted / unified access to member elements of any _tuple-like_ * @remark preferably uses a `get` member function, falling back to a * free function `get`, which is found by ADL. */ template - requires(tuple_like>) + requires tuple_like> decltype(auto) getElm (TUP&& tup) { @@ -169,7 +191,7 @@ namespace meta { using AndAll = ElmTypes>::template Rebind; template class PRED> - using OrAll = ElmTypes>::template Rebind; + using OrAny = ElmTypes>::template Rebind; }; @@ -221,7 +243,7 @@ namespace meta { * @todo 6/2025 as a first step, this replicates the implementation from C++17; * the second step would be to constrain this to a concept `tuple_like` */ - template requires(tuple_like>) + template requires tuple_like> constexpr decltype(auto) apply (FUN&& f, TUP&& tup) noexcept (can_nothrow_invoke_tup ) { @@ -242,7 +264,7 @@ namespace meta { * std::apply to unpack the tuple's contents into an argument pack and * then employ a fold expression with the comma operator. */ - template requires(tuple_like>) + template requires tuple_like> constexpr void forEach (TUP&& tuple, FUN fun) { @@ -267,7 +289,7 @@ namespace meta { * Notably this differs from #forEach, where a fold-expression with comma-operator * is used, which is guaranteed to evaluate from left to right. */ - template requires(tuple_like>) + template requires tuple_like> constexpr auto mapEach (TUP&& tuple, FUN fun) { diff --git a/src/lib/meta/tuple-helper.hpp b/src/lib/meta/tuple-helper.hpp index 25537af54..b3a95b392 100644 --- a/src/lib/meta/tuple-helper.hpp +++ b/src/lib/meta/tuple-helper.hpp @@ -33,8 +33,23 @@ ** a generic Lambda for each element of a given tuple, which allows to write ** generic code »for each tuple element«. ** + ** @remark 2026-03 this header underwent some significant rework in recent years. + ** The Lumiera project had developed a `Tuple` type way before something similar + ** was considered for inclusion into the C++ language standard; flexible and + ** specifically typed storage records were used, in conjunction with LISP-style + ** metaprogramming (»Loki Typelists«), from the very start of the project. + ** Storing an arbitrary function, combined with a tuple of arguments, was a + ** major undertaking at those days, and required a huge body of support + ** functionality — which needed to be remoulded gradually, over the + ** following years, to become compatible with newer C++ features. + ** - basic handling, like access and compile-time iteration became trivial + ** - however, the project still relies on specific metaprogramming tools, + ** to simplify handling and transforming of variadic metafunctions. + ** ** [tuple protocol]: https://en.cppreference.com/w/cpp/language/structured_binding.html#Case_2:_binding_a_type_implementing_the_tuple_operations - ** @see control::CommandDef usage example + ** @see variadic-helper.hpp + ** @see tuple-closure.hpp : usage example + ** @see feed-manifold.hpp : use of unified variadics and tuples ** @see TupleHelper_test ** @see typelist.hpp ** @see function.hpp @@ -49,9 +64,8 @@ #include "lib/integral.hpp" #include "lib/meta/tuple-concept.hpp" #include "lib/meta/variadic-helper.hpp" -#include "lib/meta/typeseq-util.hpp" -#include "lib/meta/typelist.hpp" #include "lib/meta/typelist-util.hpp" +#include "lib/meta/typeseq-util.hpp" #include "lib/meta/util.hpp" #include @@ -59,30 +73,18 @@ #include +namespace util { // forward -> see lib/format-obj.hpp ... + + template + std::string toString (TY const&) noexcept; + + template + struct StringConv; +} + namespace lib { namespace meta { - /** trait to detect tuple types */ - template - struct is_Tuple - : std::false_type - { }; - - template - struct is_Tuple> - : std::true_type - { }; - - template - struct is_Tuple> - : std::true_type - { }; - - - - - - namespace { // rebinding helper to create std::tuple from a type sequence template @@ -239,5 +241,85 @@ namespace meta { } + + + + /* ======= Tuple toString rendering ======= */ + + /** + * convenience function to dump a given tuple's contents, converted to string. + * @note this is the basic functionality to place rendered data into a `ostringstream.` + */ + template + inline void + joinTuple (TUP const& tup, std::string delim, std::ostringstream& buffer) + { + forEachIDX ([&](auto i) + { + using Elm = std::tuple_element_t; + if (i) + buffer << delim; + buffer << util::toString (getElm (tup)); + }); + } + + template + inline void + joinTuple (TUP const& tup, std::ostringstream& buffer) + { + joinTuple (tup, ","); + } + + /** convenience function to join the tuple's contents into a single string. + * @param delim optional delimiter to intersperse, defaults to `","` + */ + template + inline std::string + joinTuple (TUP const& tup, std::string delim =",") + { + std::ostringstream buffer; + joinTuple (tup, delim, buffer); + return buffer.str(); + } + + template + inline void + joinTupleParen (TUP const& tup, std::ostringstream& buffer) + { + buffer << '('; + joinTuple (tup, ",", buffer); + buffer << ')'; + } + + template + inline std::string + joinTupleParen (TUP const& tup) + { + std::ostringstream buffer; + joinTupleParen (tup, buffer); + return buffer.str(); + } }} // namespace lib::meta + + +// add a specialisation to enable tuple string conversion +namespace util { + + template + struct StringConv> + { + static std::string + invoke (std::tuple const& tuple) noexcept + try { + std::ostringstream buffer; + buffer << "«" + << typeStr(tuple) + << "»──"; + lib::meta::joinTupleParen (tuple, buffer); + return buffer.str(); + } + catch(...) { return FAILURE_INDICATOR; } + }; + +} // namespace util #endif /*LIB_META_TUPLE_HELPER_H*/ diff --git a/src/lib/meta/tuple-idx-adaptor.hpp b/src/lib/meta/tuple-idx-adaptor.hpp new file mode 100644 index 000000000..4b3b348e8 --- /dev/null +++ b/src/lib/meta/tuple-idx-adaptor.hpp @@ -0,0 +1,131 @@ +/* + TUPLE-IDX-ADAPTOR.hpp - runtime-indexed access to tuples + + Copyright (C) + 2026 Hermann Vosseler + +  **Lumiera** is free software; you can redistribute it and/or modify it +  under the terms of the GNU General Public License as published by the +  Free Software Foundation; either version 2 of the License, or (at your +  option) any later version. See the file COPYING for further details. + +*/ + +/** @file tuple-idx-adaptor.hpp + ** Adaptor to provide access to tuple elements by runtime subscript. + ** Tuples are data records with distinct fields of different type; thus + ** most of indexing, iteration and transformation of tuples need to be + ** performed statically, at compile-time. However, under the proviso that + ** the types in a given tuple are somehow _reconcilable_, a dispatcher + ** table with access functors can be built, so that effectively all fields + ** can be handled by instances of a generic functor. This setup allows to + ** define a subscript `operator[]`, so that the suitable functor is picked + ** with runtime-index and applied to the tuple to yield the indicated field, + ** albeit converted into a _common type_. + ** + ** While this is certainly a very specific setup, and incurs some cost, + ** since the functor dispatcher tables must be prepared statically, such + ** a runtime-controlled index access allows for dynamic and data-driven + ** processing of tuple_like structures, which are fully type-safe, + ** compact in storage and easy to nest. Furthermore, run-time + ** processing of arguments passed to variadic functions + ** is enabled, providing a new kind of flexible API. + ** + ** @see TupleIdxAdaptor_test + ** @see FunTrampoline_test + */ + + +#ifndef LIB_META_TUPLE_IDX_ADAPTOR_H +#define LIB_META_TUPLE_IDX_ADAPTOR_H + + +#include "include/logging.h" +#include "lib/iter-adapter.hpp" +#include "lib/meta/tuple-helper.hpp" +#include "lib/meta/common-result.hpp" +#include "lib/meta/fun-trampoline.hpp" + +#include + + +namespace lib { +namespace meta { + + template + class TupleIndexAdaptor + : public TUP + { + /** Helper to generate a dispatch table for + * this specific instance of tuple element types */ + template + struct SubscriptBuilder + { + static_assert (reconcilable + ,"A common run-time index accessor can only be built " + "if all types in the tuple can be reconciled into a " + "common type used for the access"); + + using Tuple = RefTraits::value_type; + using AddRefs = ElmTypes::template Apply; + using ResType = CommonResult_t; + using Functor = ResType(*)(Tuple&); + + template + constexpr auto + build() + { + return [](Tuple& tuple) -> ResType + { + return getElm (tuple); + }; + } + }; + + using TypeSeq = ElmTypes::Seq; + using Accessor = FunTrampoline; + + public: + using Tuple = TUP; + using ResType = Accessor::ResType; + using reference = ResType; + using value_Type = RefTraits::value_type; + static constexpr bool isRefAccess = isLRef_v; + + + /** construct by passing a source tuple */ + template + TupleIndexAdaptor(T&& dataSrc) + : TUP{forward (dataSrc)} + { } + + /// lift all constructors of the adapted tuple + using TUP::TUP; + + Tuple & tuple() { return *this; } + Tuple const& tuple() const { return *this; } + + operator std::string() const + { + return "iTup" + joinTupleParen (this->tuple()); + } + + + /* === subscriptable === */ + + static constexpr size_t size() { return Accessor::size(); } + + ResType + operator[] (size_t idx) + { + return Accessor::dispatch(idx)(*this); + } + }; + + /// deduction guide + template + TupleIndexAdaptor(TUP) -> TupleIndexAdaptor>; + + +}}// namespace lib::meta +#endif /*LIB_META_TUPLE_IDX_ADAPTOR_H*/ diff --git a/src/lib/meta/typelist.hpp b/src/lib/meta/typelist.hpp index fd80e16d1..5fa47c0db 100644 --- a/src/lib/meta/typelist.hpp +++ b/src/lib/meta/typelist.hpp @@ -115,5 +115,21 @@ namespace meta { using Seq = Types<>; }; + + + + /** + * Concept to require a generic type sequence `Types` + */ + template + concept typeseq = requires (TSEQ seq) + { + // TSEQ can invoke a function with a Types subclass + [](Types&){/*placeholder*/}(seq); + }; + + template + static constexpr bool is_Typeseq_v = typeseq; + }} // namespace lib::meta #endif diff --git a/src/lib/meta/typeseq-util.hpp b/src/lib/meta/typeseq-util.hpp index 33756c0ac..c38021480 100644 --- a/src/lib/meta/typeseq-util.hpp +++ b/src/lib/meta/typeseq-util.hpp @@ -44,6 +44,44 @@ namespace lib { namespace meta { + template + struct Prepend; + + + + /* ========= Specialisations ========= */ + + /** + * Additional specialisation of the basic type sequence type, + * allowing to re-create a (flat) type sequence from a typelist. + * @remark recursively prepend-to-tail of the type sequence. + */ + template + struct Types< Node > + { + using List = Node; + using Seq = Prepend< H + , typename Types::Seq + >::Seq; + }; + template<> + struct Types + { + using List = Nil; + using Seq = Types<>; + }; + template<> + struct Types + : Types + { }; + + + + + + + + /* ========= Information functions ========= */ /** * Find the index of the first incidence of a type in a type-sequence. @@ -82,6 +120,13 @@ namespace meta { { }; + + + + + + /* ========= Type Sequence Manipulation ========= */ + /** * Helper: prepend a type to an existing type sequence, * thus shifting all elements within the sequence @@ -99,33 +144,6 @@ namespace meta { - /** - * Additional specialisation of the basic type sequence type, - * allowing to re-create a (flat) type sequence from a typelist. - * @remark can now be built with the help of \ref Prepend. - */ - template - struct Types< Node > - { - using List = Node; - using Seq = Prepend< H - , typename Types::Seq - >::Seq; - }; - template<> - struct Types - { - using List = Nil; - using Seq = Types<>; - }; - template<> - struct Types - : Types - { }; - - - - /** Helper: separate parts of a type sequence */ diff --git a/src/lib/meta/value-type-binding.hpp b/src/lib/meta/value-type-binding.hpp index 765d62621..1103124d5 100644 --- a/src/lib/meta/value-type-binding.hpp +++ b/src/lib/meta/value-type-binding.hpp @@ -59,7 +59,6 @@ #define LIB_META_VALUE_TYPE_BINDING_H -#include "lib/error.hpp" #include "lib/meta/trait.hpp" @@ -129,48 +128,6 @@ namespace meta { using pointer = _SrcType::pointer; }; - - - - /** - * Decision helper to select between returning results by value or reference. - * - when both types can not be reconciled, _no type result_ is provided; - * this case can be detected by a compile-time bool-check - * - when one of both types is `const`, the `ResType` will be const - * - when both types are LValue-references, then the result will be a reference, - * otherwise the result will be a value type - * @see IterExplorer::expand() - */ - template>()> - struct CommonResultYield - : std::false_type - { }; - - template - struct CommonResultYield - : std::true_type - { - using _Common = std::common_type_t; - // NOTE: unfortunately std::common_type decays (strips cv and reference) - static constexpr bool isConst = isConst_v or isConst_v; - static constexpr bool isRef = isLRef_v and isLRef_v; - - using _ConstT = std::conditional_t; - using _ValRef = std::conditional_t - , std::remove_reference_t<_ConstT> - >; - - using ResType = _ValRef; - using value_type = RefTraits::value_type; - using reference = RefTraits::reference; - using pointer = RefTraits::pointer; - }; - - - + }} // namespace lib::meta #endif /*LIB_META_VALUE_TYPE_BINDING_H*/ diff --git a/src/lib/meta/variadic-helper.hpp b/src/lib/meta/variadic-helper.hpp index dcfcad5cf..f60401a5a 100644 --- a/src/lib/meta/variadic-helper.hpp +++ b/src/lib/meta/variadic-helper.hpp @@ -16,9 +16,14 @@ ** Metaprogramming with type sequences based on variadic template parameters. ** The type rebinding- and helper templates in this header allow to perform ** simple sequence manipulations on sequences of template parameters extracted - ** from variadic parameter packs. The goal is to (pre)process flexible argument - ** lists _at compile time,_ driven by template instantiation, allowing to specialise - ** and react specifically on some concrete pattern of argument types. + ** from variadic parameter packs. Among other things, this allows the sequence + ** of function arguments to be flexibly reshaped and remoulded at compile-time. + ** Driven by template instantiation, this enables adaptation in response to + ** specific argument types for each actual instance. Such metaprogramming + ** capabilities can be amplified significantly through the use of generic-λ. + ** @note with C++23, this framework could be expanded to cover also any + ** »tuple-like« product type, drawing from our non-standard extension + ** with the `concept tuple_like`, as defined in \ref tuple-concept.hpp. ** ** @remark in Lumiera, over time three different approaches were developed for ** handling sequences of types in metaprogramming; some of these techniques @@ -26,8 +31,10 @@ ** - templates with variadic arguments (e.g. std::tuple) can be manipulated directly ** - a type-sequence `Types` can be primed / rebound from other variadic templates ** - Loki-style type-lists are created from type-sequences and enable elaborate manipulations + ** ** @see feed-manifold.hpp advanced usage example in the Render Engine - ** @see TupleHelper_test + ** @see VariadicHelper_test + ** @see tuple-concept.hpp ** @see typelist.hpp ** @see function.hpp ** @see generator.hpp @@ -42,7 +49,7 @@ #include "lib/meta/typelist-util.hpp" #include "lib/meta/typeseq-util.hpp" #include "lib/meta/variadic-rebind.hpp" -#include "lib/meta/util.hpp" +#include "lib/meta/trait.hpp" namespace lib { namespace meta { @@ -160,7 +167,7 @@ namespace meta { * - the nested template #Apply wraps each type into another template * - #Rebind likewise instantiates another template with the element types * - #AndAll applies a predicate and combines the result with _logical and_ - * - #OrAll similarly evaluates _logical or_ on the application results + * - #OrAny similarly evaluates _logical or_ on the application results */ template struct ElmTypes @@ -177,7 +184,7 @@ namespace meta { template class PRED> using AndAll = std::__and_>; template class PRED> - using OrAll = std::__or_>; + using OrAny = std::__or_>; }; /** Partial specialisation to handle type sequences */ @@ -199,10 +206,23 @@ namespace meta { using AndAll = ElmTypes>::template Rebind; template class PRED> - using OrAll = ElmTypes>::template Rebind; + using OrAny = ElmTypes>::template Rebind; }; - // Note: a further specialisation for any »tuple-like« is defined in tuple-helper.hpp + // Note: a further specialisation for any »tuple-like« is defined in tuple-concept.hpp + + + + template + using Any_Const = ElmTypes::template OrAny; + template + using All_LRef = ElmTypes::template AndAll; + + template + static constexpr bool Any_Const_v = Any_Const::value; + template + static constexpr bool All_LRef_v = All_LRef::value; + diff --git a/src/steam/control/command-op-closure.hpp b/src/steam/control/command-op-closure.hpp index 19bf0201f..0a4dc04bb 100644 --- a/src/steam/control/command-op-closure.hpp +++ b/src/steam/control/command-op-closure.hpp @@ -16,6 +16,16 @@ ** Implementation of the concrete (sub)-closure of a command, responsible for ** invoking the actual command operation with the concrete (binding) arguments. ** + ** @todo 4/2026 this header seems to be more of a concept draft from the early days. + ** At that time, the C++ language did not have much function tools, did not + ** support lambdas and tuples, and thus invoking a functor object and passing + ** an arbitrary sequence of stored arguments was an innovative concept to be + ** implemented from scratch. And during that development process, some ideas + ** regarding the specific handling of relevant argument types was intermingled + ** with the creation of generic general purpose tooling. However, with the + ** switch to C++23, in summer 2025, most of that old implementation was + ** removed and replaced by standard library facilities. The #activated_ + ** flag seems to be the only piece of actual functionality here. ** @see Command ** @see command-closure.hpp ** @see command-storage-holder.hpp @@ -28,109 +38,35 @@ #define CONTROL_COMMAND_OP_CLOSURE_H #include "lib/meta/function.hpp" -#include "lib/meta/function-closure.hpp" -#include "lib/meta/tuple-accessor.hpp" -#include "lib/meta/tuple-record-init.hpp" #include "steam/control/command-closure.hpp" #include "steam/control/argument-erasure.hpp" -#include "lib/typed-allocation-manager.hpp" -#include "lib/format-cout.hpp" +#include "lib/meta/tuple-helper.hpp" +#include "lib/nocopy.hpp" -#include -#include #include #include - - namespace steam { namespace control { using lib::meta::_Fun; using lib::meta::Tuple; - using lib::meta::BuildTupleAccessor; - using lib::meta::buildTuple; - using lib::meta::Nil; - using lib::TypedAllocationManager; - using std::function; - using std::ostream; using std::string; - - - - - /** Helper for accessing an individual function parameter */ - template - < typename TY - , class BASE - , class TUP - , uint idx - > - class ParamAccessor - : public BASE - { - TY & element() { return std::get (*this); } - TY const& element() const { return std::get (*this); } - - public: - using BASE::BASE; - - - ////////////////////TODO the real access operations (e.g. for serialising) go here - - /////////////////////////////////////////////////////////////TICKET #798 : we need to pick up arguments from a lib::diff::Record. - - ostream& - dump (ostream& output) const - { - return BASE::dump (output << element() << ','); - } - }; - - template - class ParamAccessor ///< used for recursion end of implementation functions - : public TUP - { - public: - ParamAccessor (TUP const& tup) - : TUP(tup) - { } - - /** we deliberately support immutable types as command arguments */ - ParamAccessor& operator= (ParamAccessor const&) =delete; - ParamAccessor& operator= (ParamAccessor&&) =delete; - ParamAccessor (ParamAccessor const&) =default; - ParamAccessor (ParamAccessor&&) =default; - - - ////////////////////TODO the recursion-end of the access operations goes here - - ostream& - dump (ostream& output) const - { - return output; - } - }; - - - /** * closure to deal with the actual command operation. * This includes holding the invocation parameter tuple */ template class OpClosure + : util::NonAssign { - using Args = _Fun::Args; - using Builder = BuildTupleAccessor; + using Args = _Fun::Args; - using ParamStorageTuple = Builder::Product; - - ParamStorageTuple params_; + Tuple params_; bool activated_; public: @@ -138,8 +74,8 @@ namespace control { OpClosure() - : params_(Tuple()) - , activated_(false) + : params_{} + , activated_{false} { } explicit @@ -148,13 +84,6 @@ namespace control { , activated_(true) { } - /** we deliberately support immutable types as command arguments */ - OpClosure& operator= (OpClosure const&) =delete; - OpClosure& operator= (OpClosure&&) =delete; - OpClosure (OpClosure const&) =default; - OpClosure (OpClosure&&) =default; - - bool isValid () const { @@ -165,36 +94,25 @@ namespace control { /** Core operation: use the embedded argument tuple for invoking a functor * @param unboundFunctor an function object, whose function arguments are * required to match the types of the embedded ParamStorageTuple - * @note ASSERTION failure if the function signature - * doesn't match the argument types tuple. - * @note the functor might actually \em modify the param values. + * @note the functor might actually _modify the param values._ * Thus this function can't be const. */ void invoke (CmdFunctor const& unboundFunctor) { - ArgTuple& paramTuple{params_}; - std::apply (unboundFunctor.getFun(), paramTuple); + std::apply (unboundFunctor.getFun(), params_); } - operator string() const { std::ostringstream buff; - params_.dump (buff << "OpClosure(" ); - - string dumped (buff.str()); - if (10 < dumped.length()) - // remove trailing comma... - return dumped.substr (0, dumped.length()-1) +")"; - else - return dumped+")"; + buff << "OpClosure"; + lib::meta::joinTupleParen (params_, buff); + return buff.str(); } }; - - }} // namespace steam::control #endif /*CONTROL_COMMAND_OP_CLOSURE_H*/ diff --git a/src/steam/control/command-simple-closure.hpp b/src/steam/control/command-simple-closure.hpp index 5ebd56085..a81dfdbb4 100644 --- a/src/steam/control/command-simple-closure.hpp +++ b/src/steam/control/command-simple-closure.hpp @@ -38,7 +38,10 @@ #define CONTROL_COMMAND_SIMPLE_CLOSURE_H #include "lib/typed-allocation-manager.hpp" +#include "steam/control/command-closure.hpp" +#include "steam/control/argument-erasure.hpp" #include "steam/control/command-op-closure.hpp" +#include "lib/meta/tuple-record-init.hpp" #include "lib/opaque-holder.hpp" #include @@ -50,6 +53,7 @@ namespace control { namespace err = lumiera::error; using lib::InPlaceBuffer; + using lib::meta::buildTuple; using std::string; diff --git a/src/steam/control/command-storage-holder.hpp b/src/steam/control/command-storage-holder.hpp index dbb0b8d0b..81de90f25 100644 --- a/src/steam/control/command-storage-holder.hpp +++ b/src/steam/control/command-storage-holder.hpp @@ -36,9 +36,12 @@ #define CONTROL_COMMAND_STORAGE_HOLDER_H #include "lib/typed-allocation-manager.hpp" +#include "steam/control/command-closure.hpp" +#include "steam/control/argument-erasure.hpp" #include "steam/control/command-op-closure.hpp" #include "steam/control/memento-tie.hpp" #include "steam/control/command-impl-clone-builder.hpp" +#include "lib/meta/tuple-record-init.hpp" #include "lib/opaque-holder.hpp" #include @@ -50,6 +53,7 @@ namespace control { namespace err = lumiera::error; using lib::InPlaceBuffer; + using lib::meta::buildTuple; using std::string; diff --git a/src/test/event-log.cpp b/src/test/event-log.cpp index 826de4330..90941b584 100644 --- a/src/test/event-log.cpp +++ b/src/test/event-log.cpp @@ -42,7 +42,6 @@ namespace test{ namespace error = lumiera::error; - using util::stringify; using util::contains; using util::isnil; using lib::Symbol; diff --git a/src/test/test-helper.hpp b/src/test/test-helper.hpp index cea76e021..7cb136b60 100644 --- a/src/test/test-helper.hpp +++ b/src/test/test-helper.hpp @@ -19,8 +19,8 @@ ** reference kind (value, lvalue, rvalue) of a template parameter instantiation. ** For GNU compatible compilers, we expose also the interface to the internal ** ABI for [demangling type names](\ref demangleCxx). - ** - ** @note this header is included into a large number of tests. + ** @remark util::showType also uses this ABI and indicates also reference / pointer + ** @warning this header is included into a large number of tests. ** @see TestHelper_test ** @see TestHelperDemangling_test ** @@ -58,6 +58,8 @@ namespace test{ using lib::meta::disable_if; using lib::meta::demangleCxx; using lib::meta::humanReadableTypeID; + using util::showType; + using util::showTypes; constexpr auto ROUGH_PRECISION = pow (10, -3); @@ -168,116 +170,6 @@ namespace test{ - - namespace { // helper for printing type diagnostics - - template - struct TypeDiagnostics - { - using Type = X; - static constexpr auto prefix = ""; - static constexpr auto postfix = ""; - }; - template - struct TypeDiagnostics - { - using Type = X; - static constexpr auto prefix = "const "; - static constexpr auto postfix = ""; - }; - template - struct TypeDiagnostics - { - using Type = X; - static constexpr auto prefix = ""; - static constexpr auto postfix = "&"; - }; - template - struct TypeDiagnostics - { - using Type = X; - static constexpr auto prefix = ""; - static constexpr auto postfix = " &&"; - }; - template - struct TypeDiagnostics - { - using Type = X; - static constexpr auto prefix = ""; - static constexpr auto postfix = " const&"; - }; - template - struct TypeDiagnostics - { - using Type = X; - static constexpr auto prefix = "const "; - static constexpr auto postfix = " &&"; - }; - template - struct TypeDiagnostics - { - using Type = X; - static constexpr auto prefix = ""; - static constexpr auto postfix = " *"; - }; - template - struct TypeDiagnostics - { - using Type = X; - static constexpr auto prefix = "const "; - static constexpr auto postfix = " *"; - }; - template - struct TypeDiagnostics - { - using Type = X; - static constexpr auto prefix = "const "; - static constexpr auto postfix = " * const"; - }; - template - struct TypeDiagnostics - { - using Type = X; - static constexpr auto prefix = ""; - static constexpr auto postfix = " * const"; - }; - template - struct TypeDiagnostics - { - using Type = X; - static constexpr auto prefix = ""; - static constexpr auto postfix = " * const *"; - }; - } - - /** diagnostic type output, including const and similar adornments - * @warning operates after-the-fact and relies on mangled type names - * plus several heuristics. Output might thus not be entirely correct, - * especially when several levels of const, pointer and references are involved. - * If in doubt, place the TypeDebugger to reveal the type as the compiler sees it. - * @remarks the function lib::meta::typeStr removes adornments and does not work on all kinds - * of reference. This helper attempts to work around those limitations. - */ - template - inline string - showType() - { - using Case = TypeDiagnostics; - using Type = Case::Type; - - return Case::prefix - + humanReadableTypeID (typeid(Type).name()) - + Case::postfix; - } - - template - string - showTypes() - { - return "<| " + ((showType()+", ") + ... + "|>"); - } - - /** helper for investigating a variadic argument pack * @remark you can pass arguments directly to this function, * but be sure to use `std::forward(args)...` when forwarding diff --git a/tests/12metaprogramming.tests b/tests/12metaprogramming.tests index 3832abc4d..41f806e64 100644 --- a/tests/12metaprogramming.tests +++ b/tests/12metaprogramming.tests @@ -87,6 +87,11 @@ return: 0 END +TEST "Common reconciled return types" CommonResult_test < : Yes out: HasNested_Core : No @@ -133,6 +138,11 @@ return: 0 END +TEST "generate functor trampoline" FunTrampoline_test <::Inner const&&) diff --git a/tests/lib/iter-core-adapter-test.cpp b/tests/lib/iter-core-adapter-test.cpp index 06f505e4e..025074a42 100644 --- a/tests/lib/iter-core-adapter-test.cpp +++ b/tests/lib/iter-core-adapter-test.cpp @@ -22,6 +22,7 @@ #include "lib/util.hpp" #include "lib/iter-adapter.hpp" +#include "lib/meta/common-result.hpp" #include @@ -228,61 +229,26 @@ namespace test{ } - /** @test construction of a common result type. - * - based on `std::common_type` - * - so there must be some common ground - * - if any of the types is by-value, the result is - * - if any of the types is const, the result is const + /** @test when combining functors, a common result type can be detected. + * @see CommonResult_test + * @see lib/meta/common-result.hpp */ void verify_TypeReconciliation() { - using C1 = Common; - CHECK (not C1()); + using C1 = meta::CommonResult; + CHECK (not C1{}); CHECK (not C1::value); - using C2 = Common; - CHECK (not C2()); // can not be reconciled - CHECK (not C2::value); -// using X = C2::ResType; // does not (and should not) compile + using C2 = Common; + CHECK (showType() == "string"_expect ); - using C3 = Common; - CHECK (C3()); - CHECK (showType() == "string"_expect ); - - using C4 = Common; - CHECK (showType() == "string"_expect ); - - using C5 = Common; - CHECK (showType() == "string&"_expect ); // ref access to both is possible - - using C6 = Common; - CHECK (showType() == "string"_expect ); // caution, RValue might be a temporary - - using C7 = Common; - CHECK (showType() == "string"_expect ); - - using C8 = Common; - CHECK (showType() == "string const&"_expect ); - - using C9 = Common; - CHECK (showType() == "string const&"_expect ); // reconcile to const& - - using C10 = Common; - CHECK (showType() == "const string"_expect ); - - using C11 = Common; - CHECK (showType() == "const string"_expect ); // reconciled to value type - - using C12 = Common; - CHECK (showType() == "const long"_expect ); // reconciled to the larger number type - - using C13 = Common; - CHECK (showType() == "double const&"_expect ); // usual in C++ (loss of precision possible) + using C3 = Common; + CHECK (showType() == "double"_expect ); // usual in C++ (loss of precision possible) } template - using Common = meta::CommonResultYield; + using Common = meta::CommonResult; }; LAUNCHER (IterCoreAdapter_test, "unit common"); diff --git a/tests/lib/meta/common-result-test.cpp b/tests/lib/meta/common-result-test.cpp new file mode 100644 index 000000000..546a387c4 --- /dev/null +++ b/tests/lib/meta/common-result-test.cpp @@ -0,0 +1,224 @@ +/* + CommonResult(Test) - verify reconciled function return types + + Copyright (C) + 2026, Hermann Vosseler + +  **Lumiera** is free software; you can redistribute it and/or modify it +  under the terms of the GNU General Public License as published by the +  Free Software Foundation; either version 2 of the License, or (at your +  option) any later version. See the file COPYING for further details. + +* *****************************************************************/ + +/** @file common-result-test.cpp + ** unit test \ref CommonResult_test + */ + + + +#include "test/run.hpp" +#include "test/test-helper.hpp" +#include "lib/meta/tuple-helper.hpp" +#include "lib/random.hpp" + +#include "lib/meta/common-result.hpp" + +#include + + +using test::showType; +using std::string; +using std::tuple; + + +namespace lib { +namespace meta { +namespace test { + + namespace { // Dummy hierarchy + + struct II { }; + struct S1 : II { }; + struct S2 : II { }; + + template + X yield(); + } + + + + /*******************************************************************//** + * @test demonstrate the metafunction to determine a common return type. + * @see TupleIdxAdaptor_test + * @see IterExplorer_test::verify_expandOperation + * @see common-result.hpp + * @see iter-explorer.hpp + */ + class CommonResult_test : public Test + { + virtual void + run (Arg) + { + simpleUsage(); + underlyingMechanism(); + demonstrate_TypeReconciliation(); + demonstrate_Metaprocessing(); + } + + + + /** @test how to use the metafunction and the concept */ + void + simpleUsage() + { + using CT = CommonResult_t; + CHECK (showType() == "long"_expect ); + + CHECK ((not has_CommonResult_v)); + CHECK ((not reconcilable)); // Concept + + CHECK ((reconcilable_ref)); // Concept + } + + + + /** @test show the language mechanism used as foundation */ + void + underlyingMechanism() + { + auto expr = defaultGen.i(2)? defaultGen.u64() : defaultGen.uni(); + CHECK (expr >= 0); + CHECK (showType() == "double"_expect ); + + CHECK (showType() == "long"_expect ); + CHECK (showType() == "long"_expect ); + + CHECK (showType() : yield())>() == "const string"_expect ); + + CHECK (showType () : yield())>() == "II"_expect ); + CHECK (showType() : yield())>() == "II"_expect ); + } + + + /** @test show how various combinations of types are handled + * - some types can not be reconciled + * - there is no common type for an empty sequence of types + * - result falls back to return by-value (or RRef), + * unless all involved types are given by LRef + * - types that must be converted are reconciled into a value + * - C++ allows to promote integral numbers to floating-point + * - superclass / subclass relationship is observed + * - yet no automatic upcast to superclass is performed + */ + void + demonstrate_TypeReconciliation() + { + CHECK ((not CommonResult{})); // can not be reconciled + CHECK ((not CommonResult::value)); +// using X = CommonResult::Type; // does not (and should not) compile +// using X = CommonResult_t; // does not compile + CHECK ((not has_CommonResult_v)); + CHECK ((not reconcilable)); // a concept + + CHECK ((not CommonResult<>::value)); // ¬ ∃ T∈{} |… +// using X = CommonResult_t<>; // does not compile + + CHECK ((showType>()) == "string &&"_expect ); + CHECK ((showType>()) == "const string &&"_expect ); + CHECK ((showType>()) == "string &&"_expect ); + CHECK ((showType>()) == "const string &&"_expect ); + CHECK ((showType>()) == "string&"_expect ); + CHECK ((showType>()) == "string &&"_expect ); + CHECK ((showType>()) == "string const&"_expect ); + CHECK ((showType>()) == "string* &&"_expect ); + CHECK ((showType>()) == "string*&"_expect ); + CHECK ((showType>()) == "string const*&"_expect ); + CHECK ((showType>()) == "string* const&"_expect ); + CHECK ((showType>()) == "const string* &&"_expect ); + + CHECK ((showType>()) == "string &&"_expect ); + CHECK ((showType>()) == "string"_expect ); + CHECK ((showType>()) == "string &&"_expect ); + CHECK ((showType>()) == "string"_expect ); + CHECK ((showType>()) == "const string"_expect ); + CHECK ((showType>()) == "string"_expect ); + CHECK ((showType>()) == "string &&"_expect ); + CHECK ((showType>()) == "const string &&"_expect ); + + CHECK ((showType>()) == "const float* &&"_expect ); + CHECK ((showType>()) == "const float *"_expect ); + CHECK ((showType>()) == "void *"_expect ); + CHECK ((showType>()) == "void *"_expect ); + CHECK ((showType>()) == "void *"_expect ); + CHECK ((showType>()) == "long"_expect ); + CHECK ((showType>()) == "long"_expect ); + + CHECK ((showType>()) == "double"_expect ); + CHECK ((showType>()) == "double"_expect ); + CHECK ((showType>()) == "double"_expect ); + CHECK ((showType>()) == "double"_expect ); + + CHECK ((not reconcilable )); + CHECK ((not reconcilable )); + + CHECK (( reconcilable )); + CHECK (( reconcilable )); + CHECK ((not reconcilable_ref )); + CHECK ((not reconcilable_ref )); + CHECK (( reconcilable_ref )); + CHECK ((not reconcilable_ref )); + + // II ◁— S1 ∧ II ◁— S2 + CHECK (( reconcilable )); + CHECK (( reconcilable )); + CHECK (( reconcilable )); + CHECK ((not reconcilable )); + CHECK ((not reconcilable )); // NOTE: this is surprising + CHECK (( reconcilable )); // the reason is processing from left-to-right + CHECK (( reconcilable )); + + CHECK ((showType>()) == "II &&"_expect ); + CHECK ((showType>()) == "II &&"_expect ); + CHECK ((showType>()) == "II &&"_expect ); + CHECK ((showType>()) == "II &&"_expect ); + CHECK ((showType>()) == "II&"_expect ); + } + + + /** @test show use in Metaprogramming with variadics */ + void + demonstrate_Metaprocessing() + { + auto tup = std::make_tuple (1, 2.3, '4'); + CHECK (tup == "«tuple»──(1,2.3,4)"_expect ); + + using Tup = decltype(tup); + CHECK (showType() == "tuple"_expect ); + + using ValSeq = ElmTypes::Seq; + using ConstSeq = ElmTypes::template Apply; + CHECK (showType() == "Types"_expect ); + + using CommRes = CommonResult_t; + CHECK (showType() == "double"_expect ); + + // Pick function overload based on reconcilability + auto trp = std::tuple_cat (tup, tuple("😈")); + CHECK (doIt (tup) == "Yes we can"_expect ); + CHECK (doIt (trp) == "SO SAD!↯↯!"_expect ); + } + + template + requires reconcilable + static string doIt (tuple) { return "Yes we can"; } + + template + static string doIt (tuple) { return "SO SAD!↯↯!"; } + }; + + LAUNCHER (CommonResult_test, "unit common"); + + +}}} // namespace lib::meta::test + diff --git a/tests/lib/meta/fun-trampoline-test.cpp b/tests/lib/meta/fun-trampoline-test.cpp new file mode 100644 index 000000000..9b5e1af21 --- /dev/null +++ b/tests/lib/meta/fun-trampoline-test.cpp @@ -0,0 +1,113 @@ +/* + FunTrampoline(Test) - demonstrate building a static dispatcher table + + Copyright (C) + 2026, Hermann Vosseler + +  **Lumiera** is free software; you can redistribute it and/or modify it +  under the terms of the GNU General Public License as published by the +  Free Software Foundation; either version 2 of the License, or (at your +  option) any later version. See the file COPYING for further details. + +* *****************************************************************/ + +/** @file fun-trampoline-test.cpp + ** unit test \ref FunTrampoline_test + */ + + + +#include "test/run.hpp" +#include "test/test-helper.hpp" +#include "lib/meta/tuple-helper.hpp" +#include "lib/format-string.hpp" +#include "lib/format-obj.hpp" + +#include "lib/meta/fun-trampoline.hpp" + + +using test::showType; +using std::operator""s; +using std::string; +using util::_Fmt; + + +namespace lib { +namespace meta { +namespace test { + + namespace { // Builder to populate the trampoline table + + template + struct Stringify + { + using Tuple = ElmTypes::Tup; + using Functor = string(*)(Tuple const&); + + template + constexpr auto + build() + { + using ElmT = std::tuple_element_t; + + return [](Tuple const& tuple) -> string + { + static _Fmt render{"%02d: «%s»%|20t||%s"}; + return {render % idx + % util::showType() + % util::toString (getElm (tuple))}; + }; + } + }; + } + + + + /*************************************************************//** + * @test demonstrate the setup of a static functor trampoline. + * @see TupleIdxAdaptor_test + */ + class FunTrampoline_test : public Test + { + virtual void + run (Arg) + { + demonstrate_TupleAccessor(); + } + + + + /** @test build a trampoline table to access and handle tuple fields + * - [setup a builder](\ref Stringify) to populate the table. + * - use a typedef to provide the actual tuple type + * - without further ado, the trampoline is ready + */ + void + demonstrate_TupleAccessor() + { + auto tup = std::make_tuple (1, 2.3, '4', "⁵₅⁵", "😈"s); + + using Tramp = TupStringer; + CHECK (Tramp::size() == 5); + CHECK (Tramp::memSiz() == 5 * sizeof(void*)); + + CHECK (showType() == "string"_expect); + + CHECK (Tramp::dispatch(0)(tup) == "00: «int» |1"_expect ); + CHECK (Tramp::dispatch(1)(tup) == "01: «double» |2.3"_expect); + CHECK (Tramp::dispatch(2)(tup) == "02: «char» |4"_expect ); + CHECK (Tramp::dispatch(3)(tup) == "03: «const char *»|⁵₅⁵"_expect); + CHECK (Tramp::dispatch(4)(tup) == "04: «string» |😈"_expect ); + + getElm<3>(tup) = "🚀💣🔥"; + CHECK (Tramp::dispatch(3)(tup) == "03: «const char *»|🚀💣🔥"_expect); + } + + template + using TupStringer = FunTrampoline::Seq>; + }; + + LAUNCHER (FunTrampoline_test, "unit common"); + + +}}} // namespace lib::meta::test diff --git a/tests/lib/meta/late-bind-instance-test.cpp b/tests/lib/meta/late-bind-instance-test.cpp index 071cffc2c..bf3d9ee1b 100644 --- a/tests/lib/meta/late-bind-instance-test.cpp +++ b/tests/lib/meta/late-bind-instance-test.cpp @@ -19,7 +19,7 @@ #include "test/run.hpp" #include "test/test-helper.hpp" #include "test/tracking-dummy.hpp" -#include "lib/meta/tuple-accessor.hpp" +#include "lib/meta/tuple-helper.hpp" #include "lib/meta/function.hpp" #include "lib/format-cout.hpp" #include "lib/format-util.hpp" @@ -36,7 +36,7 @@ namespace test { using std::move; using std::tuple; using std::string; - using lib::meta::dump; + using lib::meta::joinTupleParen; using ::test::Tracker; @@ -160,19 +160,19 @@ namespace test { { long dummy{555}; InstancePlaceholder marker; - CHECK (dump(lateBindInstance (dummy, tuple<>{})) == "()"_expect); - CHECK (dump(lateBindInstance (dummy, tuple{42})) == "(42)"_expect); - CHECK (dump(lateBindInstance (dummy, tuple{1,2,3})) == "(1,2,3)"_expect); - CHECK (dump(lateBindInstance (dummy, tuple{marker,2,3})) == "(↗555,2,3)"_expect); - CHECK (dump(lateBindInstance (dummy, tuple{1,marker,3})) == "(1,↗555,3)"_expect); - CHECK (dump(lateBindInstance (dummy, tuple{1,2,marker})) == "(1,2,↗555)"_expect); - CHECK (dump(lateBindInstance (dummy, tuple{marker})) == "(↗555)"_expect); - CHECK (dump(lateBindInstance (dummy, tuple{string{"1"} - ,"2" - ,marker - ,tuple(2,3) - , 5.5 - })) == "(1,2,↗555,«tuple»──(2,3),5.5)"_expect); + CHECK (joinTupleParen(lateBindInstance (dummy, tuple<>{})) == "()"_expect); + CHECK (joinTupleParen(lateBindInstance (dummy, tuple{42})) == "(42)"_expect); + CHECK (joinTupleParen(lateBindInstance (dummy, tuple{1,2,3})) == "(1,2,3)"_expect); + CHECK (joinTupleParen(lateBindInstance (dummy, tuple{marker,2,3})) == "(↗555,2,3)"_expect); + CHECK (joinTupleParen(lateBindInstance (dummy, tuple{1,marker,3})) == "(1,↗555,3)"_expect); + CHECK (joinTupleParen(lateBindInstance (dummy, tuple{1,2,marker})) == "(1,2,↗555)"_expect); + CHECK (joinTupleParen(lateBindInstance (dummy, tuple{marker})) == "(↗555)"_expect); + CHECK (joinTupleParen(lateBindInstance (dummy, tuple{string{"1"} + ,"2" + ,marker + ,tuple(2,3) + , 5.5 + })) == "(1,2,↗555,«tuple»──(2,3),5.5)"_expect); } }; diff --git a/tests/lib/meta/tuple-helper-test.cpp b/tests/lib/meta/tuple-helper-test.cpp index 5fcc5ecaa..237001220 100644 --- a/tests/lib/meta/tuple-helper-test.cpp +++ b/tests/lib/meta/tuple-helper-test.cpp @@ -37,6 +37,7 @@ using test::showSizeof; using test::showType; using util::toString; using util::_Fmt; +using std::operator""s; using std::is_same_v; using std::make_tuple; using std::get; @@ -78,6 +79,7 @@ namespace test { run (Arg) { check_diagnostics(); + check_string_conversion(); check_tuple_from_Typelist(); demonstrate_generic_iteration(); verify_tuple_like_concept(); @@ -113,6 +115,21 @@ namespace test { } + /** @test apply generic string conversion, implemented by a fold-expression + */ + void + check_string_conversion() + { + auto tup = make_tuple (1, 2.3, '4', "⁵₅⁵", "😈"s); + + CHECK (showType() == "tuple"_expect); + + CHECK (joinTuple (tup,"•") == "1•2.3•4•⁵₅⁵•😈"_expect ); + CHECK (joinTupleParen (tup) == "(1,2.3,4,⁵₅⁵,😈)"_expect ); + CHECK (toString (tup) == "«tuple»──(1,2.3,4,⁵₅⁵,😈)"_expect ); + } + + /** @test verify the ability to generate tuple types from typelist metaprogramming * - the resulting types are plain flat `std::tuple` instantiations * - memory layout is identical to a POD, as expected @@ -227,7 +244,7 @@ namespace test { // Note however, the same can also be achieved simpler, // using any meta-function which takes a single type argument... CHECK (not ElmTypes::AndAll()); // ... __and_,is_integral,is_integral> - CHECK ( ElmTypes::OrAll()); + CHECK ( ElmTypes::OrAny()); } diff --git a/tests/lib/meta/tuple-idx-adaptor-test.cpp b/tests/lib/meta/tuple-idx-adaptor-test.cpp new file mode 100644 index 000000000..4aadfc220 --- /dev/null +++ b/tests/lib/meta/tuple-idx-adaptor-test.cpp @@ -0,0 +1,159 @@ +/* + TupleIdxAdaptor(Test) - demonstrate building a static dispatcher table + + Copyright (C) + 2026, Hermann Vosseler + +  **Lumiera** is free software; you can redistribute it and/or modify it +  under the terms of the GNU General Public License as published by the +  Free Software Foundation; either version 2 of the License, or (at your +  option) any later version. See the file COPYING for further details. + +* *****************************************************************/ + +/** @file fun-trampoline-test.cpp + ** unit test \ref TupleIdxAdaptor_test + */ + + + +#include "test/run.hpp" +#include "test/test-helper.hpp" +#include "lib/format-string.hpp" +#include "lib/format-util.hpp" + +#include "lib/meta/tuple-idx-adaptor.hpp" + + +using test::showType; +using std::string; +using std::tuple; + + +namespace lib { +namespace meta { +namespace test { + + + + /*************************************************************//** + * @test show run-time index access and iteration over tuples, + * relying on a static function dispatcher table. + * @see TupleIdxAdaptor_test + */ + class TupleIdxAdaptor_test : public Test + { + virtual void + run (Arg) + { + demonstrate_simpleAccess(); + demonstrate_referenceAccess(); + demonstrate_TupleIteration(); + } + + + + /** @test build adaptor to get run-time index access to a tuple + */ + void + demonstrate_simpleAccess() + { + auto tup42 = std::make_tuple (short(4), long(2)); + CHECK (tup42 == "«tuple»──(4,2)"_expect ); + + TupleIndexAdaptor idxAdapt{tup42}; + CHECK (idxAdapt.size() == 2); + CHECK (idxAdapt[0] == 4); + CHECK (idxAdapt[1] == 2); + + using ResType = decltype(idxAdapt[0]); + CHECK (showType() == "long"_expect ); + } // got access by a common value-type + + + + /** @test full access by reference to the data in the tuple is possible, + * if the data types can be reconciled to a common reference type. + */ + void + demonstrate_referenceAccess() + { + using Tup = std::tuple; + + using TupApt = TupleIndexAdaptor; + // indeed provides access by LValue reference + CHECK (TupApt::isRefAccess); + CHECK (showType() == "int&"_expect ); + + int ii{23}; + TupApt itup{-1,ii,55}; + CHECK (itup == "iTup(-1,23,55)"_expect ); + + // can access members by subscript... + CHECK (itup[0] == -1); + CHECK (itup[1] == 23); + CHECK (itup[2] == 55); + + // yet can also manipulate through this accessor + CHECK (++itup[0] == 0); + CHECK ( itup[1]++ == 23); + CHECK (++++itup[2] == 57); + + // data in the tuple fields was indeed manipulated + CHECK (itup == "iTup(0,24,57)"_expect ); + Tup copy{itup}; + CHECK (getElm<0>(copy) == 0); + CHECK (getElm<1>(copy) == 24); + CHECK (getElm<2>(copy) == 57); + + // and since the second field in the tuple is actually a reference, + // the manipulation did even affect the referred external variable... + CHECK (ii == "24"_expect ); + ++ii; + CHECK (25 == itup[1]); + CHECK (25 == getElm<1>(copy)); + } + + + /** @test Since the TupleIndexAdaptor is subscriptable, + * it can be used to build a run-time iterator over the tuple. + * @remark this demo example is somewhat weird, since the iterator is built + * based on the IdxStoreCore, which directly embeds the _source container_. + * Thus, when the iterator is copied, the source is copied as well — + * which violates several common assumptions regarding iterators. + * However, when working carefully around this irregularity, + * iteration and manipulation work as expected... + */ + void + demonstrate_TupleIteration() + { + using Tup = tuple; + using TupApt = TupleIndexAdaptor; + using TupIter = IterableDecorator>; + + // this iterator actually embeds the source tuple + TupIter tupIt{TupApt{'i','b','m'}}; + + // dereferencing this iterator provides access by reference + CHECK (showType() == "char&"_expect ); + + // can iterate as expected + CHECK (bool(tupIt)); + CHECK (*tupIt == "i"_expect ); + CHECK (util::join (tupIt, "·") == "i·b·m"_expect ); + + do + --*tupIt; // manipulate through iterator + while (++tupIt); + + CHECK (not tupIt); + // yet the manipulated data tuple is still sitting + // inside of the (now invalidated) iterator... + CHECK (tupIt == "iTup(h,a,l)"_expect ); + } + }; + + LAUNCHER (TupleIdxAdaptor_test, "unit common"); + + +}}} // namespace lib::meta::test diff --git a/tests/lib/meta/variadic-helper-test.cpp b/tests/lib/meta/variadic-helper-test.cpp index ea4774616..0d84bf1e4 100644 --- a/tests/lib/meta/variadic-helper-test.cpp +++ b/tests/lib/meta/variadic-helper-test.cpp @@ -85,7 +85,7 @@ namespace test { CHECK (showType< S1AA >() == "__and_, is_pointer >"_expect); CHECK (false == S1AA::value); - using S1OA = S1::OrAll; + using S1OA = S1::OrAny; CHECK (showType< S1OA >() == "__or_, is_pointer >"_expect); CHECK (false == S1OA::value); @@ -108,7 +108,7 @@ namespace test { CHECK (showType< S0AA >() == "__and_ >"_expect); CHECK (true == S0AA::value); - using S0OA = S0::OrAll; + using S0OA = S0::OrAny; CHECK (showType< S0OA >() == "__or_ >"_expect); CHECK (true == S0OA::value); @@ -131,7 +131,7 @@ namespace test { CHECK (showType< S2AA >() == "__and_, is_pointer, is_pointer >"_expect); CHECK (true == S2AA::value); - using S2OA = S2::OrAll; + using S2OA = S2::OrAny; CHECK (showType< S2OA >() == "__or_, is_pointer, is_pointer >"_expect); CHECK (true == S2OA::value); @@ -153,7 +153,7 @@ namespace test { CHECK (showType< S3AA >() == "__and_, is_pointer, is_pointer >"_expect); CHECK (false == S3AA::value); - using S3OA = S3::OrAll; + using S3OA = S3::OrAny; CHECK (showType< S3OA >() == "__or_, is_pointer, is_pointer >"_expect); CHECK (true == S3OA::value); } diff --git a/tests/lib/parse-test.cpp b/tests/lib/parse-test.cpp index 54e1d0ba2..b241e1d3a 100644 --- a/tests/lib/parse-test.cpp +++ b/tests/lib/parse-test.cpp @@ -31,7 +31,7 @@ namespace test { using ::test::showType; using lib::meta::typeSymbol; - using lib::meta::is_Tuple; + using lib::meta::is_Tuple_v; using std::decay_t; using std::vector; using std::get; @@ -183,7 +183,7 @@ namespace test { CHECK ( e2.result); using SeqRes = decltype(e2)::Result; // Note: the result type depends on the actual syntax construction - CHECK (is_Tuple()); // Result model from sequence is the tuple of terminal results + CHECK (is_Tuple_v); // Result model from sequence is the tuple of terminal results auto& [r1,r2] = *e2.result; CHECK (r1.str() == "hello"_expect); CHECK (r2.str() == "world"_expect); diff --git a/tests/lib/verb-visitor-dispatch-test.cpp b/tests/lib/verb-visitor-dispatch-test.cpp index ed778a6ab..96fe7989f 100644 --- a/tests/lib/verb-visitor-dispatch-test.cpp +++ b/tests/lib/verb-visitor-dispatch-test.cpp @@ -22,7 +22,7 @@ #include "lib/format-string.hpp" #include "lib/format-cout.hpp" #include "lib/format-util.hpp" -#include "lib/meta/tuple-accessor.hpp" +#include "lib/meta/tuple-helper.hpp" #include "lib/iter-explorer.hpp" #include @@ -108,7 +108,7 @@ namespace test{ report (Literal func, ARGS&&...args) { return string(func) - + meta::dump (make_tuple (forward(args)...)); + + meta::joinTupleParen (make_tuple (forward(args)...)); } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 0ba2df810..47d205731 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -53273,8 +53273,9 @@ + - + @@ -55270,6 +55271,7 @@ + @@ -61007,11 +61009,39 @@ + + + + + + + + + + + + + + + + + + + + +

+ Habe dennoch eine eigene Lösung entwickelt, da ich mit den Tools aus der STDLIB nicht zurechtgekommen bin: diese sind komplex, haben eine schwer verständliche Spezifikation und sind auf zwei Spezialfälle ausgerichtet. Meine Implementierung, lib::meta::CommonResult verwendet die gleiche unterliegende Programmiertechnik (nämlich die type-reconciliation durch den ternären-Operator). Aber sie verwendet nur diese Technik, ohne weitere Magie, ist dafür aber auch mit unseren Typ-Sequenzen integrierbar +

+ +
+ +
+
@@ -61067,6 +61097,7 @@ + @@ -61105,6 +61136,24 @@ + + + + +

+ Technologie mal beiläufig ausgearbeitet, als Basis für einen laufzeit-indizierten Tuple-Accessor (⟹ Tuple runtime-Iteration vs. compile-time traversal). Anlaß war die Überlegung, wie man eine iter::chain()-Funktion bauen könnte. +

+

+ +

+

+ 2026: für all das gibts noch keinen wirklichen Bedarf +

+ +
+ + +
@@ -61140,8 +61189,8 @@ - - + + @@ -61165,6 +61214,48 @@ + + + + +

+ commit 20392eee1c3bf5300c6956db27cbcf6722f67a0b +

+

+ Author: Ichthyostega <prg@ichthyostega.de> +

+

+ Date:   Sat Jun 7 18:04:59 2025 +0200 +

+

+ +

+

+ clean-up: successfully replaced the old fixed type sequence (closes: #987) +

+

+     +

+

+ This resolves an intricate problem related to metaprogramming with variadic templates and function signatures. Due to exceptional complexity, a direct solution was blocked for several years, and required a better organisation of the support code involved; several workarounds were developed, gradually leading to a transition path, which could now be completed in an focused clean-up effort over the last week. +

+

+ Metaprogramming with sequences of types is organised into three layers: +

+
    +
  • + simple tasks can be solved with the standard facilities of the language, using pattern match with variadic template specialisations +
  • +
  • + the ''type-sequence'' construct `Types<T...>` takes the centre stage for the explicit definition of collections of types; it can be re-bound to other variadic templates and supports simple direct manipulation +
  • +
  • + for more elaborate and advanced processing tasks, a ''Loki-style type list'' can be obtained from a type-sequence, allowing to perform recursive list processing task with a technique similar to LISP. +
  • +
+ +
+
@@ -61189,6 +61280,1880 @@
+ + + + + + +

+ Tuple oder Typsequenzen sind heterogen, aber zur Compile-Zeit festgelegt und abzählbar. Gewünscht ist, darauf zugreifen zu können wie auf einen homogenen Laufzeit-Container mit random-Access, also nach dem Array-Muster. Darauf aufbauend könnte man dann eine Laufzeit-Iteration machen, und damit Auswertungen, Suchen, Verzweigungen etc. +

+

+ +

+

+ Eine Grundvoraussetzung ist, daß man die heterogenen Typen irgendwie in den Griff bekommt. Dieses Problem läßt sich nicht generisch lösen, es hängt also vom Einzelfall ab, in dem man eben zusätzliche Informationen hat, wie z.B. einen common type,  oder man mappt erst einen Funktor darauf (mit meta::ElmTypes), der eine gemeinsame Interpretation herstellt (z.B. toString)... +

+ +
+ +
+ + + + + + + + + + +

+ Das erschien mir der »offensichtliche« Ansatz, da man ja seit C++17 auch static constexpr mit einem direkten Initialisierer definieren kann. Die Funktoren sollten sowiso in den statischen Speicher, denn das ist fast schon wie ein Code-Block (im einfachsten Fall wird der Compiler nur Sprungadressen codieren die in den Assembly für jede Lambda-Instanz führen). +

+

+ +

+

+ Ist das wieder mall eine »offensichtlich« over-engineered-Lösung (wenn ich schon sage, etwas sei „offensichtlich“) ⟹ Websuche zeigt, auf die Idee sind andere Leute auch schon mehrfach gekommen +

+ +
+
+ + + + + + +

+ ...damit ich mich nicht festlegen muß auf eine spezielle Art von Produkttyp. Sofern man auch tuple-concept.hpp mit dazunimmt, sollte die Technik automatisch auch für jedes »tuple_like« funktionieren +

+ +
+
+ + + + + + + +

+ Das würde der Design-Ebene widersprechen; eine solche Trampolin-Tabelle ist ein generischer Baustein. Der konkrete Typ wird erst im jeweiligen Funktor relevant — wobei auch der durchaus conceptisiert-sein könnte +

+ +
+
+ + + +
+ + + + + + + + + + + + + + + +

+ Die Entscheidung für einen weitgehend statischen Fall beruht auf meinem Verständnis der Aufgabe. Man könnte es auch anders handhaben; ich sehe aber keinen Grund dafür. Die Größe einer solchen Trampolin-Tabelle ist eigentlich irrelevant, man möchte sie nicht als Daten herumschieben (und streng genommen müßte man auch den Assembly-Code der konkreten Funktionsinstanzen mit zum »Footprint« rechnen). +

+

+ +

+

+ Zur Erinnerung: ich modelliere dieses ganze neue Konstrukt anhand der Beispiel-Anwendung »Laufzeit-indizierter Zugriff auf ein Tupel«. Mithin ist dieser Fall gekennzeichnet +

+
    +
  • + durch den Builder, der den Fall generisch repräsentiert +
  • +
  • + durch die konkrete Sequenz der Typen für die eine Tabelle erzeugt wird +
  • +
+ +
+ + +
+ + + + + +

+ Denn wir wollen ein Setup schaffen, in dem ein bestimmtes Bindhing hinter einem Marker-Typ angehängt ist, um es dann bei Bedarf vom Compiler für jeweils gegebenen Typ-Sequenzen automatisch instantiiert zu können +

+ +
+
+ + + + + + +
+ + + + + + + + + + + +

+ sonst müßte ich nämlich verschiedene Varianten definieren, und diese dann durch Metaprogramming erkennen (was u.U. sogar unmöglich ist) +

+ +
+ + + + +

+ Begründung: eine sehr spezielle Methoden-Signatur wird erwartet. +

+

+ Zwar könnte man die auch über ein Lambda erzeugen, aber weder die Definition, noch der Aufruf wären intuitiv — insofern ziehe ich eine gute-alte benannte Methode vor. +

+ +
+
+ + + + + + + + + + + + + + + + + + + + + +

+ Je nachdem, wie effizient eine solche komplett inline gespeicherte Funktor-Abstraktion ist, könnte eine solche Lösung relevant werden für eine verallgemeinerte Trampolin-Tafel — nicht jedoch für den konkreten Anwendungsfall, den ich zur Modelliuerung verwende. Denn hier läßt sich ohne große Schwierigkeiten ein einheitlicher Funktor konstruieren, welcher lediglich auf eine Index-Nummer statisch parametrisiert werden muß. +

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

+ denn auf diesen Typ wird das Trampoline-Array typisiert +

+ +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +

+ ...ist inzwischen nicht einmal mehr klar, ob das Bug oder Feature ist; jedenfalls ist lib::meta::typeStr so geschrieben, daß es const, * und & absorbiert. Und typeStr wird an hundertausend Stellen verwendet... +

+ + +
+
+ + + + + +

+ Vom Umfang der Features her ist das möglich, und das eine spezialisierte Template werden wir uns wohl noch leisten können (im Zweifelsfall gibt es durchaus andere Sachen, die könnten aus meta/Util in die Traits) +

+ + +
+ +
+ + + + +

+ Erst dachte ich meta/util.hpp, aber da gibt es kein lib::Literal, und außerdem habe ich dann doch etwas Bauchweh mit dem großen Template. Demgegenüber erscheint format-object.hpp der natürliche Platz, da sind auch toString() und typedString() und demangleCxx() +

+ + +
+
+
+
+ + + + + + +
+
+ + + + + + + + +

+ und zwar für den IterExplorer-Expander: +

+
    +
  • + siehe _ExpanderTraits +
  • +
  • + implementiert in value-type-binding.hpp +
  • +
  • + das Trait-Template dort: CommonResultYield hat bereits eine sehr elaborierte Logik, die für diesen Fall passend erscheint: +
  • +
      +
    • + erkennt ob std::common_type erfolgreich war (⟶ das ist nicht trivial und ich mußte einen eigenen duck-Detector dafür bauen) +
    • +
    • + leider impliziert common_type ein decay ⟹ muß Referenzen und const eigens handhaben +
    • +
    • + kann per non-const-ref nur liefern, wenn das für alle beteiligten Typen geht +
    • +
    • + fällt defensiv zurück auf const und by-value result +
    • +
    +
+ +
+ + + + + + +

+ Da std::common_type automatisch den decay_t anwendet, dachte ich damals, ich kann die Referenzen und die Const-ness einfach nachträglich fixen. Das führt aber in gewissen Randfällen zu einem gefährlich falschen Ergebnis +

+ + +
+
+ + + + + +

+ die STDLIB macht ein »decay« (für common_type) bzw. hat elaborierte Logik, um mit RValue-Referenzen umzugehen. +

+ + +
+
+
+ + + + + + + + + + + + + + + + + +

+ ...mit dem default non-type-Argument am Ende der Liste; dort habe ich den Test versteckt, ob es überhaupt ein Common-Result gibt +

+ +
+
+ + + + + + + + +

+ ...da hab ich schon oft darüber nachgedacht (vergesse es aber immer wieder): eine (Lumier)-Typsequenz ist ein richtiger Typ, während ein variadic-argument-pack ein sonderbares Meta-Konstrukt ist, das nur per Pattern-Manching verarbeitet werden kann +

+ +
+ + + +
+ + + + + + + + +

+ ...und zwar wendet es zunächst Decay an, und extrahiert dann das Verhalten des ternären-conditionals (· ? · : · ), was bereits elaobrierte Regeln spezifiziert, um Ergebnistypen zu vereinheitlichen. Eine leere Menge hat keinen common-type. Spezieller twist bei einer einelementigen Menge: auch hier wird auf das Verhalten des ternären Operators delegiert +

+ +
+
+ + + + + + + + + + + +

+ Das ist hier ein Fall, wo der Code klarer wird, und obendrei die »Hilfsfunktion« eigentlich der Kern ist, und auch für sich selbst nürzlich. Hinzu kommt, daß ich die Basis-Definition über ein geschicktes Mapping mit ElmTypes realisieren kann, ohne überhaupt eine Processing-Logik anschreiben zu müssen. Dieses Konstrukt ist jedoch ziemlich technisch, und profitiert davon, wenn es als eigenständige Definition mit Namen gegeben ist. +

+ +
+ +
+ + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ Seinerzeit war das Gedankenlosigkeit, weil es im gleichen Kontext verwendet wird (nämlich ein Trait in IterExplorer). Aus der gleichen Gedankenlosigkeit heraus hatte ich auch gleich noch die RefTraits in den CommonResultYield mit eingebaut. +

+ +
+
+ + + + +

+ ...nämlich auf dem Level von IterExplorer. Dort sind auch bereits die Function-Tools  da, und damit implizit auch schon Typelist und Typeseq.... +

+ + +
+ +
+ + + + +

+ So eine Spezialisierung, die in irgend einem Basis-Header versteckt ist, empfinde ich immer als hochproblematisch; man verwendet sie meist, ohne zu realisieren, daß sie existiert, und man findet sie im Zweifelsfall nur schwer, besonders wenn das Feature viel verwendet wird +

+ + +
+
+ + + + +

+ Es bildet sich nämlich immer mehr ein eigener Status heraus, auf dem erst die Typlisten- und Sequenz-Manipulationen stattfinden. Dieser wird seit etwa einem Jahr durch das ElmTypes-Template definiert. Und die Tuple-Metafunktionen liegen noch eine Ebene höher +

+ + +
+
+ + + + +

+ Was ich allerdings auch dann wieder gut finde, denn ich benenne die Namespaces dann lieber inhaltlich, denn sie sollen ja auch dann noch für eine gewisse Trennung sorgen, wenn alle Ebenen der Library eingeschlossen sind +

+ + +
+
+
+
+
+ + + + + + + + +

+ ...damit das Feature als Solches in diesem Kontext gezeigt wird, denn inhaltlich  wird es oft relevant, wenn man aus einer StateCore heraus Werte aus verschiedenen Quellen kombiniert +

+ + +
+ +
+ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +

+ Grundsätzlich soll meine Metafunktion den Basismechanismus  exponieren +

+ + +
+
+ + + + + + + +
+
+

+ src/lib/iter-explorer.hpp:423:24: error: static assertion failed: source iterator and result from the expansion must yield compatible values +

+

+ 423 | static_assert (can_reconcile, +

+

+ | ^~~~~~~~~~~~~ +

+

+ src/lib/iter-explorer.hpp:423:24: note: 'lib::{anonymous}::_ExpanderTraits<lib::iter_explorer::BaseAdapter<lib::iter_explorer::StlRange<lib::Several<std::reference_wrapper<steam::engine::ProcNode> >&> >, lib::IterExplorer<lib::iter_explorer::BaseAdapter<lib::iter_explorer::StlRange<lib::Several<std::reference_wrapper<steam::engine::ProcNode> >&> > > >::can_reconcile' evaluates to false +

+

+ src/lib/iter-explorer.hpp:428:15: error: no type named 'Type' in 'using lib::{anonymous}::_ExpanderTraits<lib::iter_explorer::BaseAdapter<lib::iter_explorer::StlRange<lib::Several<std::reference_wrapper<steam::engine::ProcNode> >&> >, lib::IterExplorer<lib::iter_explorer::BaseAdapter<lib::iter_explorer::StlRange<lib::Several<std::reference_wrapper<steam::engine::ProcNode> >&> > > >::_CommonT = struct lib::meta::CommonResult<std::reference_wrapper<steam::engine::ProcNode>&, std::reference_wrapper<steam::engine::ProcNode>&>' {aka 'struct lib::meta::CommonResult<std::reference_wrapper<steam::engine::ProcNode>&, std::reference_wrapper<steam::engine::ProcNode>&>'} +

+

+ 428 | using YieldRes = _CommonT::Type; +

+

+ | ^~~~~~~~ +

+

+ +

+
+
+ + +
+ +
+ + + + +
+
+

+ string +

+

+ ProcID::genSrcSpec (Leads&  leads) const +

+

+ { +

+

+ return isnil(leads)? string{"-◎"}  // no leads => starting point itself is a source node +

+

+                    : "┉┉{" +

+

+                      + util::join( +

+

+                          explore(leads) +

+

+                            .expandAll([](ProcNode&  n){ return explore(watch(n).leads());  }) +

+

+ ...                                                                             ▲▲▲▲▲ Compile-Error +

+

+ ... +

+
+
+ +
+ +
+ + + + +

+ Hier liegen mehrere Schichten von Typen übereinander... +

+
+
+

+ +

+

+ using ProcNodeRef = std::reference_wrapper<ProcNode>; +

+

+ using Leads = lib::Several<ProcNodeRef>; +

+

+ +

+
+
+

+ Und lib::Several definiert seinen value_type und reference per RefTraits. Also kommt hier eine Referenz auf einen std::ReferenceWrapper<ProcNode> raus. Welcher selber wiederum konvertierbar ist auf ProcNode& ... Und dieses Setup wird dann von den Type-Traits in IterExplorer analysiert. +

+ + +
+ + + + + +

+ beide Züge im Expander liefern praktisch den gleichen Lumiera-Iterator-Typ, der durch Dekorieren über den Iterator von lib::Several entsteht: +

+

+ BaseAdapter<StlRange<Several<reference_wrapper<ProcNode> >& >> +

+

+ Daraus sollte sich in beiden Fällen der gleiche Iterator-Yield ergeben: +

+

+ reference_wrapper<ProcNode>& +

+

+
+

+ + +
+
+ + + + + + + + + + +

+ aka 'struct lib::meta::CommonResult<std::reference_wrapper<steam::engine::ProcNode>&, std::reference_wrapper<steam::engine::ProcNode>&>' +

+ + +
+
+
+ + + + + + + + + + +

+ da die Lösung mit std::declval<T>() ja funktioniert ⟹ Problem vertagen +

+ + +
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + + +

+ weil wir den Typ über tuple_element_t<idx> finden +

+ +
+
+
+ + + + +

+ ...damit es leicht inline verwendbar ist +

+ +
+
+
+ + + + + + +

+ das könnte man dann auf Tuple& setzen +

+ +
+ + + + + + + + + + + + + + + +

+ Das ist hier ein Kniff, der funktioniert, weil der TupleIndexAdapter tatsächlich auf das Tuple herunter-sliced werden kann +

+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +

+ das mit den Referenzen habe ja ich dazugebaut +

+ +
+
+ + + + +

+ mir dämmert warum der C++-Standard hier ein decay eingebaut hat... +

+ +
+ +
+
+ + + + + + +
+
+

+ research/try.cpp:190:50: error: cannot bind non-const lvalue reference of type 'lib::meta::TupleIndexAdaptor<std::tuple<long int, short int> >::SubscriptBuilder<lib::meta::Types<long int, short int> >::ResType' {aka 'long int&'} to a value of type 'short int' +

+

+ 190 | return getElm<idx> (tuple); +

+

+ +

+
+
+ +
+
+
+
+ + + + + + + + + + + +

+ Die Referenz impliziert, daß ich mit/an den Objekten etwas machen möchte. Die Iteration impliziert, daß ich aber nicht gezielt auf einzelne Positionen im Tupel zugreifen möchte, wie sonst normalerweise immer. Aber ich möchte eben auch nicht zunächst einzeln durchgehen, und jedes Element eigens auf ein gemeinsames Interface adaptieren — sondern das soll automatisch passieren. +

+ +
+
+
+ + + +
+
+ + + + + + +

+ für uns hier relevant: auf was könnten Rückgabetypen  vereinigt werden? +

+ +
+ + + + + + + + +

+ d.h. der ternäre-Operator macht die eigentliche Arbeit. Und ein gemeinsamer Super-Typ wird ebenfalls nicht gefunden +

+ +
+
+ + + + + + + + + + + + + + + +

+ und da __true_type eine nested typedef type = Param hat, taucht dort der Common-Type dann im Ergenistyp auf; dagegen wenn der ternäre Operator keinen Typ finden kann, dann erbt common_type von dem __false_type und hat daher keine nested typedef +

+ +
+
+
+ + + + + + + +

+ mache eine Typedef CommonResultType +

+ +
+ + + +
+
+

+ template<typename X> +

+

+ X yieldRes(); +

+

+ +

+

+ template<typename X,  typename U> +

+

+ using CommonResultType =  decltype(true? yieldRes<X>()  : yieldRes<U>()  ); +

+

+ +

+
+
+ +
+
+ + + + + + +

+ Die ganze komplexe Verbindung von std::common_type mit den const und lref-Checks ist unnötig (und gefährlich, wie ich gesehen habe). Stattdessen kann man alles in einem einzigen Anlauf von dem ternären-Operator machen lassen. +

+ +
+ + + + + + + + + +

+ beispielsweise const Subclass& und Superclass&  schafft der Compiler nicht zu vereinigen, obwohl doch die offensichtliche Lösung const Superclass& wäre +

+ +
+
+ + + + +

+ ...in der üblichen Weise, indem man einen Typfindungs-Algorithmus spezifziert. Leider ist daraus für mich nicht unmittelbar ersichtlich, warum Manches nicht klappt. Ist es nur, weil dieser Fall nicht in dem Algo erfaßt wird, oder gibt es einen tieferen Grund, warum es gar nicht geht, oder ist nur die GCC-Implementierung unzulänglich? +

+ +
+
+
+
+ + + + + + + + + +

+ ja schon ... aber ist technisch sehr mühsam +

+ +
+ + + + +

+ Problem-1 : Rekursion korrekt ansetzen +

+ +
+ + + + +

+ transitive Hülle — nein das wäre explizit falsch +

+ +
+
+ + + + + + + + + + +

+ ...denn da brauchen wir bereits den CommonType von den ersten zwei Typen, um den Tail-Ausdruck auch nur anschreiben zu können. +

+ +
+
+ + + + + + + + + + + + + + + + + +

+ Also denjenigen zwei Typen, die aktuell geprüft werden. Wenn nicht wenigstens einer der Typen direkt in diesem Check-Konstrukt gesetzt wird, kann nämlich der Compiler den Auswertungs-Ausdruck bereits instantiieren, wenn er das Template beginnt zu instantiieren. Da hab ich ein paar Stunden gebraucht, um das zu „schnallen“ — die Fehlermeldungen sagen einem nämlich nicht direkt, aus welchem Kontext heraus die Instanz erfolgt; es wird nur unbestimmt moniert, daß man diesen ternären Operator nicht konstruieren kann. Und was noch verwirrender ist: für manche Situationen fängt das der requires-Ausdruck ab, für andere jedoch nicht, und man bekommt dann sogar einen Compile-Fehler aus dem Template. Im Rückblick wird mir das klar, denn in diesen Fällen ist es  kein Argument-Substitutions-Fehler, sondern ein allgemeiner Compile-Fehler im Rumpf eines Templates, das der Compiler schon mal nebenbei mit übersetzt. Nur wenn man eine Abhängikeit auf den unmittelbar zu prüfenden Typ herstellt, nur dann wird das zu einem Substitutions-Fehler, und dann klappt der Test auch. +

+ +
+ +
+ + + + +

+ Das ist nicht nur elegant, sondern vmtl. die einzige Möglichkeit, eine kompakte Formulierung hinzubekommen für mehr als zwei Typen: letztlich erzeugt man in einer Typ-Auswertung diesen Result-Typ und das front-End-Template kann davon erben.... In diesem Result ist dann entweder die nested typedef für das Ergebnis enthalten, oder eben nicht +

+ +
+ +
+
+
+ + + + + + + + + + + + + +

+ damit wird alles nun dramatisch vereinfacht +

+ +
+ + + + + + + + + + + + + + + + +

+ ...das alte Problem. Es ist schwerer, fremden Code (und in dem Fall hier, auch fremde formale Specs) zu verstehen, als selber was zu schreiben.... +

+

+ +

+

+ In dem Fall hier kommt dazu, daß das Verhalten kritisch ist, und ein Fehler zu Data-corruption führen kann. Insofern ist mir eine einfachere Implementierung, die ich zu 100% verstehe, lieber als eine elaborierte Implementierung, die vielleicht auch noch dies und das zusätzlich kann. Bei meiner Implementierung bin ich mir nun sicher, daß sie wasserdicht ist. +

+ +
+
+ + + + + +

+ meine Implementierung hier beruht ausschließlich auf dem ternären-Operator +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +

+ ...ich weiß noch nicht, wofür ich das brauche; und es sind viele Fragen offen, wie z.B. ob es eine Referenz-Variante braucht, und wie genau man einen const-iterator definiert. +

+ +
+ + +
+ + + + +

+ using TupIt = lib::IterableDecorator<lib::IdxStoreCore<IdxApt>>; +

+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ denke vor allem an die Weiterungen, wie Referenz, Const, weitere Methoden; sowas möchte man nicht an einer Komponente entwickeln, die in einem zentralen utility-Header sitzt. (Hab ich grad eben wieder schmerzhaft gemerkt, bzgl. der Iter-Adapter) +

+ +
+
+ + + + + + + + + + + + + + +

+ das Quell-Objekt liegt im Iterator +

+ + +
+ + + +

+ Meine vorausgegangenen Untersuchungen zur generischen Implementierung einer flatten()-Operation hat mir gezeigt, daß man sich damit in einer gefährlichen Grenzzone bewegt — sobald diese eingebette Quelle irgendwohin kopiert wird, kann die Logik brechen +

+ + +
+ +
+ + + + + + + +

+ ...implementiere den default-Ctor explizit und setze i_ ≔ size +

+ + +
+ +
+
+ + + + + + +

+ ...weil das auf einer Kopie des Iterators arbeitet, die durch den Aufruf von begin(it) erzeugt wird — immerhin funktioniert das Iterations-Ende nun, nachdem ich den Kniff für den default-konstruierten IdxStoreCore angewendet habe +

+ + +
+ +
+ + + + + + +
+
+
+ + + + + + +

+ ...im Besonderen durch mein neues Framework mit einem allgemeinen Concept tuple_like und direkt dazu passendem apply(f, tuple), forEach(tuple, fun) und mapEach(tuple, fun). Bevor man solchen Code zuverlässig bauen konnte (ermöglicht durch C++17 mit den fold-Expressions), war ein sehr viel schwerfälligerer Ansatz mit Template-Metaprogramming der einzige Weg, um generische Verarbeitungsschritte auf ein beliebiges Tupel anzuwenden. Und das bedingte stets, daß der Client irgend ein (meist trickreiches und schwer-verständliches) Template spezialisieren mußte. Mit den neuen Tools besteht diese Möglichkeit implizit immer noch, denn die angewendete »Funktion« muß ja ein generischer Funktor sein. +

+ +
+
+ + + + + + +

+ Stand (allgemein) +

+ +
+ + + +

+ Als ich vor einem Jahr den Sprung gemacht habe, war ich natürlich vorsichtig... +

+

+ Bisher ist mit GCC-14 keinerlei »Desaster« passiert, obwohl ich mich in den letzten Tagen schon mal punktweise aus der Deckung getraut habe. In einem Fall gabs eine Compile-Failure, die m.E. eigentlich von der requires{}-Expression abgefangen werden sollte (die aber nicht wirklich eine substution-Failure war). +

+

+ Ich möchte auch weiterhin vorsichtig bleiben, und nur einige wenige Concepts an kritischen Stellen einschleifen. Der nächste große Schritt wäre dann, ein Concept für den Lumiera-Iterator auf den Weg zu bringen. Da habe ich noch etwas Angst davor, denn wenn damit was schief geht, bin ich handlungsunfähig. Also wäre es angemessener, zunächst noch etwas mehr damit zu experimentieren, wie man Concepts als Interface verwenden kann. +

+

+ +

+

+ Abgesehen davon macht mir Eclipse enige Sorgen; die neue Editor-Variante mit Language-Server fühlt sich nach »Steinzeit« an, und die alte Variante wird mit C++20 stellenweise instabil und die Lookup-Indices sind unzuverlässig geworden. Sowas kann ein massives Risiko für mich selber bei der Arbeit darstellen. Mit JetBrains-Tools kann ich nicht Arbeiten, da werde ich wahnsinnig und aggressiv, und fühle mich wie ein Anfänger bevormundet. VisualStudioCode wäre eine Lernkurve, und ich weiß nicht, ob es mir schmeckt. Bliebe nur Emacs (mit dem ich mal sehr glücklich war, wenn er halt nich so »anders herum« wäre, in jeder Hinsicht). Aber für Emacs ist auch das Language-Server-Tooling wohl noch problematisch (wie mir Benny im Detail berichtet hat). +

+ +
+ + +
+ + + + +

+ wenn ich diesen Code heute sehe, schäme ich mich — muß mir dann aber klar machen, daß das vor 10 Jahren eine besondere Leistung war, so etwas bereits völlig generisch auf die Beine gestellt zu haben. +

+ +
+ +
+ + + + +

+ Diesen Zusammenhang sollte ich auf jeden Fall beseitigen — so ein wichtiges Basis-Tool soll nicht einen so »mühsamen« Header mit sich herumschleppen +

+ +
+
+ + + + + + + + + + + +

+ das ist ein echtes »Wenn«.... +

+

+ Immerhin habe ich ja jetzt ein Concept tuple_like — und der reine Check auf ein Concept hat bisher stets völlig problemlos funktioniert... +

+ + +
+
+ + + + +
+ + + + + + + + +

+ das ist eine Typ-Alias, die direkt per Typlist / Typsequenz gebaut werden kann; es ist tatsächlich ein std::tuple +

+ + +
+ +
+ + + + +
+ + + + + + + + + +

+ das ist also ein Experiment: werde ich mich daran erinnern können, oder doch immer ElmTypes nehmen? +

+ + +
+ + + +
+
+ + + + + + + +

+ hat zwei signifikante Verwendungen +

+ + +
+ + + + + + + + + + + + + + + + +

+ man könnte eine neue Implementierung machen, die dann ein generisches Lambda nimmt; dieses hätte aber eine wesentlich implizitere Schnittstelle und müßte die Typen von seinen Argumenten abgreifen, bzw. man müßte auch diesem Lambda ein explizites Typ-Argument geben — das bedeutet, die Labda-basierte Lösung wäre hier verwirrender und nicht unbedingt eine Verbesserung +

+ + +
+
+ + + + +
+
+ + + + + + + + +

+ ...was daran liegt, daß das Command-System angelegt wurde, lange bevor es Tupel gab; +

+

+ +

+

+ generell ist das Command-System »on hold« +

+
    +
  • + es ist grundsätzlich fragwürdig, in welcher Form es mit CQRS verbunden sein wird +
  • +
  • + die Implementierung ist komplett »ok-ish« („heut würd ich es nicht mehr so machen“), gründlich getestet und ausgereift. +
  • +
  • + die meisten Features in diesem System — vor allem das partielle Binden von Commands — erscheinen mir an der Aufgabe vorbei entwickelt, weil man es eben machen kann (und damals die Funktor-Tools für mich neu und geil waren). Aus heutiger Sicht erscheint es viel wahrscheinlicher, daß man soche Aufgaben im vorbeigehen mit einem Labda erschlägt.... +
  • +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ baut einen Dekorator-Chain über das Tupel als Basistyp +

+ + +
+ +
+ + + + + + + + +

+ ...wie so oft in diesen ersten Jahren; ich hab über Wochen oder gar Monate diverse Frameworks gebaut, nur um dann — schließlich, endlich — festzustellen, daß ich mir nicht im Entferntesten vorstellen kann, wie ich mit diesem Framework das weiterhin ungreifbare Problem zu fassen bekomme. +

+ + +
+
+ + + + + + + + + + + + + + + + + +

+ Da war das der FunctionApplicator / die FunctionClosure, die an dieser Stelle erzeugt wurde. +

+ + +
+ +
+
+
+ + + + + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ ich erinnere mich: hatte jahrelang damit gekämpft, +

+

+ einen variadischen Overload von join bereitzustellen +

+ + +
+ + + +

+ ...was letztlich nur zu endlosen compile-Problemen in speziellen Randfällen führte — und schließlich zu der Einsicht, daß es ein gänzlich anderes Tool ist, und auch entsprechend anders heißen sollte; seitdem heißt es stringify() und liefert einen Container mit Strings +

+ + +
+
+
+ + + + + + + + + + + + + + + + + +

+ eigentlich fällt hier nur eine compile-time Sequenz von string-Werten an +

+ + +
+ + + + + + + +

+ Fragen: +

+
    +
  • + gibt es ein modernes, einfaches und klares API, mit dem man eine Sequenz von Werten an einen Container anhängt? +
  • +
  • + könnte man generisch, einfach und effizient von einem Lumiera-Iterator in einen Input-Iterator »entladen«? +
  • +
  • + wäre das eine denkbare Variante für das IterExplorer::effuse()-Terminal? +
  • +
+ + +
+ + +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + +

+ was ich jetzt nicht mache... +

+ + +
+ + + + + + + +
+
+
@@ -61259,8 +63224,9 @@ - + + @@ -61275,6 +63241,11 @@ + + + + + @@ -61390,6 +63361,87 @@
+ + + + + + +

+ Es sollten die gleichen, bewährten und effizienten Strutkuren zum Einsatz kommen, wie sie eine typische std::function-Implementierung bereitstellt. Abweichend davon sollte es aber niemals Heap-Storage geben, sondern der Functor sollte stets koplett in einem inline-Buffer liegen. Insofern wäre zusätzliche Infrastruktur notwendig, um ein solches Objekt »abzuwerfen«; ggfs. gäbe es auch mehrere standard-Größen  für Funktoren, die dann einheitlich behandelt werden könnten. +

+ +
+ + + + +

+ Bisher komme ich mit den zur Verfügung stehenden Techniken gut über die Runde. Zwar wäre es sicher mal spannend, sowas selber zu entwickeln, würde aber auch einen Aufwand für Recherche, Konzeption, Test und Verbesserungen erfordern. Ein möglicher Nutzen wäre im Rahmen eines Custom-Allocators, wenn tatsächlich jede zusätzliche externe Storage ausgeschlossen werden muß. Konkret ist für lib::Several eine solche spezielle Storage geplant, und wird auch eine zentrale Rolle in der RenderEngine spielen — mit entsprechend signifikantem Hebel. Die bisherige Platzhalter-Implementierung für die lib::Several-Storage verwendet std::function und geht auch sonst grotesk verschwenderisch mit dem Speicher um. +

+ +
+ + +
+ + + + + + + +

+ ein Funktions-Pointer und ein normaler Funktionsaufruf sind praktisch äquivalent +

+ +
+ + + +

+ Hier gibt es mutmaßlich einige subtile Unterschiede, die sich in µ-Benchmarks zeigen, für die ich jedoch keine allgemeingültige Tendenz erwarte. +

+
    +
  • + ein normaler Function-Call springt an eine konstante Adresse, die direkt im Assembly liegt. Zur Ausführung muß diese Adresse natürlich aus dem Code-Segment in die CPU geladen werden, dann folgt ein spezieller Maschinenbefehl. Sie wird dort vom Loader eingepatcht, nachdem das Code-Objekt reolziert wurde. Für die Performance ist überwiegend relevant, ob die angesprungene Code-Page bereits im Prozessor-Cache liegt. Falls allerdings die angesprungene Funktion im gleichen Executable liegt, könnte—  theoretisch — ein global arbeitender Optimiser die Anordnung der aufgerufenen Code-Abschnitte verbessern. +
  • +
  • + Ein Funktionspointer dagegen verwendet eine Sprungadresse, die vorher bereits als Datenwert in ein Register gebracht wurde. Auch dieser Datenwert wurde irgendwoher geladen, aber er ist erst zur Laufzeit bekannt (das ist der zentrale Unterschied). Deshalb kann auch der Optimiser hier kaum etwas machen (es sei denn er erkennt eine monomorphic translation und ersetzt einen bekanntermaßen konstanten Funktionspointer durch einen direkten Call). Abgesehen davon ist auch für einen Funktionspointer-Call das Laden der Code-Segmente in den CPU-Cache der entscheidende Performance-Faktor +
  • +
  • + beide Fälle unterscheiden sich von einer Inline-Funktion, die keinerlei Argument-Übergabe machen muß und keine externen Code-Segmente zu laden hat — weshalb sie mindestens um den Faktor 10 schneller ist. +
  • +
+

+ Sieht man von den feinen Unterschieden bezüglich Optimierung ab, ist für normale Funktion und Funktionspointer der Ablauf äquivalent: Parameter bereitstellen, Sprungaddresse laden (i.d.R. bereits im Cache), anderes Codesegment anspringen (oft nicht im Cache) +

+ +
+
+ + + + +

+ ...und zwar ist das der Sprung in den »Invoker«. Das ist der Aufruf eines Funktionspointers, und bekommt zusätzlich zur Signatur auch noch eine Instanz-Adresse. Oder eben nicht, wenn in dem Funktor tatsächlich ein reiner Funktionspointer steckt. Der Code des Invokers ist jeweils anders für jeden unterschiedlichen Typ des aufgerufenen Funktor-Objekts. Ist der invocation-Operators des Zielt-Funktor-Objekts inline, dann kann er in die Invocator-Implementierung geinlined werden. Sonst erfolgt dahinter eben ein weiterer delegierender Funktionsaufruf — also wieder eine Indirektion mehr als bei direktem Aufruf. Ist in den Funktor zudem eine Member-Pointer gebunden, oder wurden Argumente closed, dann ist dahinter ein weiterer, delegierender Funktor notwendig +

+ +
+
+ + + + +

+ Denn für den Aufruf der virtuellen Methode muß zunächst eine Hilfsfunktion aufgerufen werden, um die VTable zu bekommen, und aus dieser wird ein Funktionspointer genommen und angesprungen. Das entspricht in etwa dem Aufwand für den Aufruf über einen Invoker (für std::function). Sobald aber mehrere Funktionen abstrahiert werden sollen, ist die VTable effizienter. +

+ +
+
+
+
+
@@ -141998,7 +144050,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -169561,6 +171613,15 @@ std::cout << tmpl.render({"what", "World"}) << s + + + + + + + + + @@ -170988,6 +173049,17 @@ class Something + + + + + + + + + + + @@ -172224,6 +174296,10 @@ unsigned int ThreadIdAsInt = *static_cast<unsigned int*>(static_cast<vo + + + + @@ -205602,7 +207678,174 @@ env.Command('out.bin', 'in.bin', my_action) - + + + + + + +

+ Entstanden sind diese Ebenen aus dem Bestreben, die Compile-Zeiten klein zu halten; ich achte penibel auf die Gegenwart bestimmter »Leit-Header«: +

+
    +
  • + util.hpp +
  • +
  • + meta/trait.hpp +
  • +
  • + iter-adapter.hpp +
  • +
  • + meta/function.hpp +
  • +
  • + meta/variadic-helper +
  • +
  • + meta/tuple-helper +
  • +
+

+ Diese Gewohnheit hat sich nach Bauchgefühl herausgebildet und ist nicht empirisch fundiert — systematisch Messungen zur Compile-Zeit und Executable-Größe habe ich noch nie gemacht. Allerdings orientiert sich die Einteilung daran, wie schmerzhaft es ist, in einem dieser Leit-Header etwas zu ändern. Denn das impliziert, wie weitreichend dieses Stratum der Library verwendet wird. +

+ +
+ + + + + + + + + + + + + + + + + + +

+ commit 84377102322b128a710cc19617f68f583adc314b +

+

+ Author: Ichthyostega <prg@ichthyostega.de> +

+

+ Date:   Wed Feb 25 23:20:26 2026 +0100 +

+

+ +

+

+     BufferProvider: research how to build a policy with callbacks +

+

+     +

+

+     While it is trivially simple to define some struct with callback functions, +

+

+     such a definition turns out to be obnoxiously complex and hard to read in pracice, +

+

+     thereby nullifying any benefit from the shorthand λ-based notation. +

+

+     +

+

+     This kind of problem with the readability at client-side is quite common +

+

+     when using callbacks for a policy-based design. To overcome this difficutly, +

+

+     it would be necessary to define the policy and the callbacks in one +

+

+     succinct builder expression. Unfortunately such a builder syntax +

+

+     typically requires to write a lot of boilerplate template code. +

+

+     +

+

+     After several failed experiments, I have found a bold new way to +

+

+     exploit features of C++20 in conjunction with a macro, allowing +

+

+     to generate a suitable data record directly form an C++ expression. +

+ +
+ +
+
+ + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +