Library: prepare two wrapper-helpers to cover corner cases

Since the new `flatten()` operation was implemented in the `IterExplorer` framework,
some non-obvious corner cases must be considered; notably the source iterator
might yield something that is not by itself a Lumiera Iterator, yet could be
adapted by applying those generic mechanisms already in place to handle such cases;
however there is the special twist in the case at hand that we do not have to adapt
the ''source iterator'', yet rather the ''product'' of the source iterator —
a requirement that, once understood, can be easily adopted with
yet another iterator-decorator layered below.
This commit is contained in:
Fischlurch 2026-03-28 18:09:24 +01:00
parent 00098dd815
commit d3407094be
8 changed files with 1449 additions and 61 deletions

View file

@ -448,15 +448,15 @@ namespace lib {
* @see iter-explorer-test.hpp
* @see iter-adaptor-test.cpp
*/
template<class ST, typename T =iter::CoreYield<ST>>
template<class ST, typename RES =iter::CoreYield<ST>>
class IterStateWrapper
{
ST core_;
public:
using value_type = meta::RefTraits<T>::value_type;
using reference = meta::RefTraits<T>::reference;
using pointer = meta::RefTraits<T>::pointer;
using value_type = meta::RefTraits<RES>::value_type;
using reference = meta::RefTraits<RES>::reference;
using pointer = meta::RefTraits<RES>::pointer;
IterStateWrapper (ST&& initialState)
: core_(std::forward<ST>(initialState))
@ -477,9 +477,9 @@ namespace lib {
}
/* === lumiera forward iterator concept === */
/* === Lumiera Forward Iterator concept === */
T
RES
operator*() const
{
__throw_if_empty();
@ -489,13 +489,13 @@ namespace lib {
pointer
operator->() const
{
if constexpr (meta::isLRef_v<T>)
if constexpr (meta::isLRef_v<RES>)
{
__throw_if_empty();
return & core_.yield(); // core interface: yield
}
else
static_assert (!sizeof(T),
static_assert (!sizeof(RES),
"can not provide operator-> "
"since iterator pipeline generates a value");
}

View file

@ -56,9 +56,9 @@ namespace lib {
public:
using pointer = meta::ValueTypeBinding<IT>::pointer;
using reference = meta::ValueTypeBinding<IT>::reference;
using value_type = std::remove_reference<reference>::type; ///< @note will be const for const iterators (while const_iterator::value_type isn't)
using pointer = iter::Trait<IT>::pointer;
using reference = iter::Trait<IT>::reference;
using value_type = iter::Trait<IT>::value_type;
CursorGear()

View file

@ -486,6 +486,8 @@ namespace lib {
/* ========= IterExplorer pipeline building blocks ========= */
/**
* @internal Base of pipe processing decorator chain.
* IterExplorer allows to create a stack out of various decorating processors
@ -517,38 +519,74 @@ namespace lib {
};
/**
* @internal Iterator adapter to _virtually project_ an arbitrary type \a RES
* onto the result of the underlying Lumiera Iterator, by means of a force-cast.
*/
template<class SRC, typename RES>
class RedressAdapter
: public SRC
{
static_assert(can_IterForEach<SRC>::value, "Lumiera Iterator required as source.");
using SrcRes = iter::Yield<SRC>;
static_assert(meta::isLRef_v<SrcRes>, "Source iterator must yield its result by-ref.");
public:
using value_type = meta::RefTraits<RES>::value_type;
using reference = meta::RefTraits<RES>::reference;
using pointer = meta::RefTraits<RES>::pointer;
reference
operator*() const
{
return reinterpret_cast<reference> (SRC::operator*());
}
pointer
operator->() const
{
return & operator*();
}
using SRC::SRC;
};
/**
* @internal Decorator for IterExplorer adding the ability to "expand children".
* The expandChildren() operation is the key element of a depth-first evaluation: it consumes
* one element and performs a preconfigured _expansion functor_ on that element to yield
* its "children". These are given in the form of another iterator, which needs to be
* compatible to the source iterator ("compatibility" boils down to both iterators
* yielding a compatible value type). Now, this _sequence of children_ effectively
* replaces the expanded source element in the overall resulting sequence; which
* means, the nested sequence was _flattened_ into the results. Since this expand()
* operation can again be invoked on the results, the implementation of such an evaluation
* requires a stack datastructure, so the nested iterator from each expand() invocation
* can be pushed to become the new active source for iteration. Thus the primary purpose
* of this Expander (decorator) is to integrate those "nested child iterators" seamlessly
* into the overall iteration process; once a child iterator is exhausted, it will be
* popped and iteration continues with the previous child iterator or finally with
* the source iterator wrapped by this decorator. The source pipeline is only pulled
* once the expanded children are exhausted.
* The expandChildren() operation is the key element of a _depth-first evaluation:_
* it consumes one element and performs a preconfigured _expansion functor_ on that element
* to yield its "children". These are given in the form of another iterator, which needs to be
* compatible to the source iterator (within this context, "compatibility" boils down to both
* iterators yielding a compatible value type). Now, this _sequence of children_ effectively
* replaces the expanded source element in the overall resulting sequence; which means that
* the nested sequence was _flattened_ into the results. Since this expand() operation can
* again be invoked on the results, the implementation of such an evaluation requires a
* **stack datastructure**, so the nested iterator from each expand() invocation can be
* _pushed on top_ in order to become the new active source for iteration. Thus the prime
* purpose of this Expander (decorator) is to integrate those "nested child iterators"
* seamlessly into the overall iteration process; once a child iterator is exhausted,
* it will be _popped_ and iteration continues with the previous child iterator, or,
* finally, with the source iterator wrapped by this decorator. Overall, the given
* source pipeline is only pulled, whenever the expanded children are exhausted.
* @remark since we allow a lot of leeway regarding the actual form and definition of the
* _expansion functor_, there is a lot of minute technical details, mostly confined
* within the _FunTraits traits. For the same reason, we need to prepare two different
* bindings of the passed raw functor, one to work on the source sequence, and the other
* one to work on the result sequence of a recursive child expansions; these two sequences
* need not be implemented in the same way, which simplifies the definition of algorithms.
* @tparam SRC the wrapped source iterator, typically a IterExplorer or nested decorator.
* @tparam FUN the concrete type of the functor passed. Will be dissected to find the signature
* _expansion functor_, various technical details must be observed, and these minutiae
* are largely maintained within the _FunTraits traits. For the same reason, we need to
* prepare two different bindings of the passed raw functor, where one of them is used
* to work on the source sequence, while the other one handles the result sequence of
* _recursive child expansions;_ these two sequences need not be implemented precisely
* in the same way — what matters here is compatible behaviour — and this flexible
* setup helps to simplify the definition of search- and evaluation algorithms.
* @tparam SRC the wrapped source iterator, typically an iterator adaptor or nested decorator.
* @tparam FUN actual type of the functor passed. Will be type-matched to detect its signature.
* @note the _return type_ of #yield depends _both_ on the return type produced from the original
* sequence and the return type of the sequence established through the expand functor.
* An attempt is made to _reconcile these_ and this attempt may fail (at compile time).
* The reason is, any further processing downstream can not tell if data was produced
* by the original sequence of the expansion sequence. Notably, if one of these two
* delivers results by-value, then the Expander will _always_ deliver all results
* by-value, because it would not be possible to expose a reference to some value
* The rationale is that any further processing downstream can not tell if a data result
* was produced by the original sequence of the expansion sequence. Notably, if one of
* these two delivers results by-value, then the Expander will _always_ choose to deliver
* all its results by-value, since it is impossible to expose a reference to some value
* that was just delivered temporarily from a source iterator.
*/
template<class SRC, class RES>
@ -727,7 +765,7 @@ namespace lib {
/**
* @internal extension to the Expander decorator to perform expansion delayed on next iteration.
* @internal extension to the Expander decorator to perform a delayed expansion on next iteration.
*/
template<class SRC>
class ScheduledExpander
@ -844,7 +882,7 @@ namespace lib {
/**
* @internal Decorator for IterExplorer to map a transformation function on all results.
* @internal Decorator for IterExplorer to map a transformation function onto all results.
* The transformation function is invoked on demand, and only once per item to be treated,
* storing the treated result into an universal value holder buffer. The given functor
* is adapted in a similar way as the "expand functor", so to detect and convert the
@ -871,10 +909,10 @@ namespace lib {
TransformFunctor trafo_;
TransformedItem treated_;
public:
using value_type = meta::ValueTypeBinding<RES>::value_type;
using reference = meta::ValueTypeBinding<RES>::reference;
using pointer = meta::ValueTypeBinding<RES>::pointer;
public: // Note: result might be a nested iterator -- not using ValueTypeBinding here!
using value_type = meta::RefTraits<RES>::value_type;
using reference = meta::RefTraits<RES>::reference;
using pointer = meta::RefTraits<RES>::pointer;
template<typename FUN>
@ -907,6 +945,7 @@ namespace lib {
t1.treated_.reset();
t2.treated_.reset();
swap (t1.trafo_, t2.trafo_);
swap (t1.srcIter(), t2.srcIter());
}
@ -961,6 +1000,125 @@ namespace lib {
/**
* Iterator adapter to package each iterator result into a **wrapper object** of type \a WRA.
* Built as a simplified variant of the Transformer, it can not invoke an arbitrary function,
* yet will destroy the previous wrapper and construct a new wrapper into the embedded storage
* buffer on each iteration.
* @note similar as the Transformer, WrapperAdapter will _re-engage_ after copy, move and swap.
* This implies to discard a wrapped object residing in the ItermWrapper's storage; the next
* result-pull will then cause it to be regenerated from source, since the absolute storage
* locations both of the ItemWrapper storage, and the underlying source iterator has changed.
* Notably this protects against insidious data corruption in cases where some part of the
* implementation (typically indirectly) happens to take a reference into the source.
* @warning this mechanism might have surprising ramifications, especially when the underlying
* iterator has generated a collection by-value; this data will be discarded whenever the
* iterator pipeline is moved, which might happen automatically when adding further layers.
* Notably there is an interference with possible iterator decorators _added downstream_,
* that need to pull ahead for grouping and filtering.
* @remark Users of WrapperAdapter, and of IterExplorer are responsible in general to ensure
* that any embodied structure preferably relies on value copy of iterator results rather,
* and otherwise only employs references into stable memory locations.
*/
template<class SRC, typename WRA>
class WrapperAdapter
: public SRC
{
using WrappedItem = wrapper::ItemWrapper<WRA>;
WrappedItem wrapped_{/*created empty*/};
public: // Note: result might be a nested iterator -- not using ValueTypeBinding here!
using value_type = meta::RefTraits<WRA>::value_type;
using reference = meta::RefTraits<WRA>::reference;
using pointer = meta::RefTraits<WRA>::pointer;
WrapperAdapter (SRC&& dataSrc)
: SRC{move (dataSrc)}
{ }
WrapperAdapter() =default;
WrapperAdapter (WrapperAdapter const& o)
: SRC{o}
, wrapped_{/* deliberately empty: force re-engage */}
{ }
WrapperAdapter (WrapperAdapter && o)
: SRC{move (o)}
, wrapped_{/* deliberately empty: force re-engage */}
{ }
WrapperAdapter&
operator= (WrapperAdapter changed)
{
swap (*this,changed);
return *this;
}
friend void
swap (WrapperAdapter& t1, WrapperAdapter& t2)
{
using std::swap;
t1.wrapped_.reset();
t2.wrapped_.reset();
swap (t1.srcIter(), t2.srcIter());
}
/* === decorated Lumiera Forward Iterator === */
reference
operator*() const
{
return unConst(this)->reWrapIt();
}
pointer
operator->() const
{
return & operator*();
}
WrapperAdapter&
operator++()
{
wrapped_.reset(); // discard current wrapper
++ srcIter();
return *this;
}
/** refresh state when other layers manipulate the source sequence */
void
expandChildren()
{
wrapped_.reset();
SRC::expandChildren();
}
ENABLE_USE_IN_STD_RANGE_FOR_LOOPS (WrapperAdapter);
friend bool
operator== (WrapperAdapter const& w1, WrapperAdapter const& w2)
{
return w1.srcIter() == w2.srcIter();
}
private:
SRC&
srcIter() const
{
return unConst(*this);
}
reference
reWrapIt ()
{
if (not wrapped_) // build a wrapper instance once per src item
wrapped_ = * srcIter();
return *wrapped_;
}
};
/**
* @internal Decorator for IterExplorer to group consecutive elements into fixed sized chunks.
* One group of elements is always prepared eagerly, and then the next one on iteration.
@ -1083,14 +1241,15 @@ namespace lib {
/**
* @internal Decorator for IterExplorer to group consecutive elements controlled by some
* grouping value \a GRP and compute an aggregate value \a AGG for each such group as
* **grouping value** \a GRP and compute an aggregate value \a AGG for each such group as
* iterator yield. The first group is consumed eagerly, each further group on iteration;
* thus when the aggregate for the last group appears as result, the source iterator has
* already been exhausted. The aggregate is default-initialised at start of each group
* and then the computation functor \a FAGG is invoked for each consecutive element marked
* with the same _grouping value_ — and this grouping value itself is obtained by invoking
* the functor \a FGRP on each source value. All computation are performed on-the-fly. No
* capturing or reordering of the source elements takes place, rather groups are formed
* already been exhausted. The aggregate is default-initialised at start of each group,
* followed by invoking the [computation functor](\ref #Aggregator) for each consecutive
* element that has been marked with the same _grouping value_ — and this grouping value
* itself is obtained by invoking the [grouping functor](\ref #Grouping) on each value
* produced by the underlying source iterator. All computation is performed on-the-fly.
* No capturing or reordering of the source elements takes place, rather groups are formed
* based on the changes of the grouping value over the source iterator's result sequence.
* @tparam AGG data type to collect the aggregate; must be default constructible and assignable
* @tparam GRP value type to indicate a group
@ -1099,7 +1258,7 @@ namespace lib {
* This limitation was deemed acceptable (adapting a function with several arguments would
* require quite some nasty technicalities). The first argument of this `aggFun` refers
* to the accumulator by value, and thereby also implicitly defines the aggregate result type.
* @warning the Aggregator \a AGG *must not capture references* to upstream internal state, because
* @warning the #Aggregator *must not capture references* to upstream internal state, because
* the overall pipeline will be moved into final location _after the initial ctor call._
*/
template<class SRC, typename AGG, class GRP>
@ -1189,7 +1348,7 @@ namespace lib {
* Similar to the Transformer, the given functor is adapted as appropriate. However,
* we require the functor's result type to be convertible to bool, to serve as approval test.
* The filter predicate and thus the source iterator is evaluated _eagerly_, to establish the
* *invariant* of this class: _if a "current element" exists, it has already been approved._
* **invariant** of this class: _if a "current element" exists, it has already been approved._
*/
template<class SRC>
class Filter
@ -1282,7 +1441,7 @@ namespace lib {
* capturing it _by value._ After building, this remoulded version can be assigned to the
* original filter functor, under the assumption that both are roughly compatible. Moreover,
* since we wrap the actual lambda into an adapter, allowing for generic lambdas to be used
* as filter predicates, this setup allows for a lot of leeway regarding the concrete predicates.
* as filter predicates, this setup provides a lot of wiggle room for defining predicates.
* @note whenever the filter is remoulded, the invariant is immediately
* [re-established](\ref Filter::pullFilter() ), possibly forwarding the sequence
* to the next element approved by the new version of the filter.

View file

@ -739,9 +739,9 @@ namespace lib {
return bool(source_);
}
using pointer = ValueTypeBinding<VAL>::pointer;
using reference = ValueTypeBinding<VAL>::reference;
using value_type = ValueTypeBinding<VAL>::value_type;
using pointer = meta::RefTraits<VAL>::pointer;
using reference = meta::RefTraits<VAL>::reference;
using value_type = meta::RefTraits<VAL>::value_type;
};

View file

@ -0,0 +1,611 @@
/*
TUPLE-ACCESSOR.hpp - metaprogramming utilities for type and data tuples
Copyright (C)
2016 Hermann Vosseler <Ichthyostega@web.de>
  **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-accessor.hpp
** Metaprogramming with tuples-of-types and the `std::tuple` record.
** This header complements typelist.hpp and provides a bridge from type sequences
** to the tuple type provided by the standard library, including traits and
** helpers to build tuple types from metaprogramming and to pretty-print tuples.
**
** Notably the handling of tuples is based on a **Concept** `tuple_like`,
** that is satisfied for any type in compliance with the »tuple protocol«.
** Together with a [generic accessor][\ref lib::meta::getElm], this allows
** to handle all _tuple-like_ types uniformly.
** This solution reaches beyond the scope of the C++ standard, which declines
** to define such a concept and rather provides an "exposition-only" concept
** that is limited to a hard-wired selection of library types, yet does not
** cover custom user provided types able to participate in _structured bindings_.
** @note Due to that unfortunate limitation of the standard, we're forced to
** provide our own alternative implementation of `std::apply`.
**
** Furthermore, a generic iteration construct is provided, to instantiate
** a generic Lambda for each element of a given tuple, which allows to write
** generic code »for each tuple element«.
**
** @see control::CommandDef usage example
** @see TupleHelper_test
** @see typelist.hpp
** @see function.hpp
** @see generator.hpp
**
*/
#ifndef LIB_META_TUPLE_ACCESSOR_H
#define LIB_META_TUPLE_ACCESSOR_H
#include "lib/meta/typelist.hpp"
#include "lib/meta/typelist-util.hpp"
#include "lib/meta/typeseq-util.hpp"
#include "lib/meta/variadic-helper.hpp"
#include "lib/meta/util.hpp"
#include <tuple>
#include <utility>
#include <functional>
namespace util { // forward declaration
template<typename TY>
std::string
toString (TY const& val) noexcept;
}
namespace lib {
namespace meta {
/** trait to detect tuple types */
template<typename T>
struct is_Tuple
: std::false_type
{ };
template<typename...TYPES>
struct is_Tuple<std::tuple<TYPES...>>
: std::true_type
{ };
template<typename...TYPES>
struct is_Tuple<const std::tuple<TYPES...>>
: std::true_type
{ };
using std::remove_cv_t;
using std::is_reference_v;
using std::remove_reference_t;
/** @internal building-block: a type supporting the `tuple_size` metafunction */
template<class TUP>
concept tuple_sized = requires
{
{ std::tuple_size<TUP>::value } -> std::convertible_to<size_t>;
};
/** @internal building-block: a type where elements can be accessed through a `get` friend function */
template<class TUP, std::size_t idx>
concept tuple_adl_accessible = requires(TUP tup)
{
typename std::tuple_element_t<idx, TUP>;
{ get<idx>(tup) } -> std::convertible_to<std::tuple_element_t<idx, TUP>&>;
};
/** @internal building-block: a type where elements can be accessed through a `get` member function */
template<class TUP, std::size_t idx>
concept tuple_mem_accessible = requires(TUP tup)
{
typename std::tuple_element_t<idx, TUP>;
{ tup.template get<idx>() } -> std::convertible_to<std::tuple_element_t<idx, TUP>&>;
};
template<class TUP, std::size_t idx>
concept tuple_element_accessible = tuple_mem_accessible<TUP,idx> or tuple_adl_accessible<TUP,idx>;
template<class TUP>
concept tuple_accessible =
tuple_sized<TUP> and
WithIdxSeq<std::tuple_size_v<TUP>>::andAll([](auto idx)
{
return tuple_element_accessible<TUP,idx>;
});
/**
* Concept to mark any type compliant to the »tuple protocol«
*/
template<class TUP>
concept tuple_like = not is_reference_v<TUP>
and tuple_sized<remove_cv_t<TUP>>
and tuple_accessible<remove_cv_t<TUP>>;
/**
* Helper for abstracted / unified access to member elements of any _tuple-like_
* @remark preferably uses a `get<i>` member function, falling back to a
* free function `get`, which is found by ADL.
*/
template<std::size_t idx, class TUP>
requires(tuple_like<std::remove_reference_t<TUP>>)
decltype(auto)
getElm (TUP&& tup)
{
using Tup = std::remove_reference_t<TUP>;
static_assert (0 < std::tuple_size_v<Tup>);
if constexpr (tuple_mem_accessible<Tup,0>)
{
if constexpr (std::is_reference_v<TUP>)
return tup.template get<idx>();
else
{ // return value copy when tuple given as RValue
using Elm = std::tuple_element_t<idx, TUP>;
Elm elm(tup.template get<idx>());
return elm;
}
}
else
{ // ▽▽▽ ADL
using std::get;
return get<idx> (std::forward<TUP> (tup));
}
}
namespace { // apply to tuple-like : helpers...
/** @internal invocation helper similar to C++17 — but preferring a custom `get` impl */
template<typename FUN, typename TUP, size_t...Idx>
constexpr decltype(auto)
__unpack_and_apply (FUN&& f, TUP&& tup, std::index_sequence<Idx...>)
{
return std::invoke (std::forward<FUN> (f)
,getElm<Idx> (std::forward<TUP>(tup))...
);
}
/** @internal invoke a metafunction with \a FUN and all element types from the _tuple-like_ \a TUP */
template<template<typename...> class META, class FUN, class TUP>
struct _InvokeMetafunTup
{
using Tupl = std::decay_t<TUP>;
using Elms = ElmTypes<Tupl>::Seq;
using Args = Prepend<FUN, Elms>::Seq;
using Type = RebindVariadic<META, Args>::Type;
};
template<template<typename...> class META, class FUN, class TUP>
struct _InvokeMetafunTup<META, FUN, TUP&>
{
using Tupl = std::decay_t<TUP>;
using Elms = typename ElmTypes<Tupl>::template Apply<std::add_lvalue_reference_t>;
using Args = Prepend<FUN, Elms>::Seq;
using Type = RebindVariadic<META, Args>::Type;
};
template<class FUN, class TUP>
inline constexpr bool can_nothrow_invoke_tup = _InvokeMetafunTup<std::is_nothrow_invocable,FUN,TUP>::Type::value;
}
/**
* Replacement for `std::apply` — yet applicable to _tuple-like custom types_.
* For unclear reasons, the standard chooses to reject such custom types, and
* only allows a fixed set of explicitly defined facilities from the Stdlib
* (tuple, pair, array, and some ranges stuff).
* @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<class FUN, class TUP> requires(tuple_like<remove_reference_t<TUP>>)
constexpr decltype(auto)
apply (FUN&& f, TUP&& tup) noexcept (can_nothrow_invoke_tup<FUN,TUP> )
{
using Indices = std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<TUP>>>;
return __unpack_and_apply (std::forward<FUN> (f)
,std::forward<TUP> (tup)
,Indices{}
);
}
/**
* Tuple iteration: perform some arbitrary operation on each element of a tuple.
* @note the given functor must be generic, since each position of the tuple
* may hold a data element of different type.
* @remark credits to David Vandevoorde (member of C++ committee) for using
* std::apply to unpack the tuple's contents into an argument pack and
* then employ a fold expression with the comma operator.
*/
template<class TUP, class FUN> requires(tuple_like<remove_reference_t<TUP>>)
constexpr void
forEach (TUP&& tuple, FUN fun)
{
lib::meta::apply ([&fun]<typename...ELMS>(ELMS&&... elms)
{
(fun (std::forward<ELMS>(elms)), ...);
}
,std::forward<TUP> (tuple));
}
/**
* Apply some arbitrary function onto all elements of a tuple.
* @return a new tuple constructed from the results of this function
* @note the functor must be generic and able to work with all element types
* @warning pay attention to references; the function can take arguments by-ref
* and manipulate them (side-effect), and it may return references,
* which will be placed as such into the result tuple; furthermore,
* the argument tuple can also be taken as reference...
* @remark The tuple constructor invocation is preceded by a regular argument evaluation,
* which has unspecified evaluation order (even in C++17); _no assumptions_ can be
* made regarding the order the functor will see the source tuple elements.
* Notably this differs from #forEach, where a fold-expression with comma-operator
* is used, which is guaranteed to evaluate from left to right.
*/
template<class TUP, class FUN> requires(tuple_like<remove_reference_t<TUP>>)
constexpr auto
mapEach (TUP&& tuple, FUN fun)
{
return lib::meta::apply ([&fun]<typename...ELMS>(ELMS&&... elms)
{ //..construct the type explicitly (make_tuple would decay fun result types)
using Tuple = std::tuple<decltype(fun (std::forward<ELMS>(elms))) ...>;
return Tuple (fun (std::forward<ELMS>(elms)) ...);
}
,std::forward<TUP> (tuple));
}
/**
* Specialisation of variadic access for any tuple-like
* @see variadic-helper.hpp
*/
template<tuple_like TUP>
struct ElmTypes<TUP>
{
template<typename>
struct Extract;
template<size_t...idx>
struct Extract<std::index_sequence<idx...>>
{
using ElmTypes = Types<std::tuple_element_t<idx,TUP> ...>;
};
static constexpr size_t SIZ = std::tuple_size_v<TUP>;
using Idx = std::make_index_sequence<SIZ>;
using Seq = Extract<Idx>::ElmTypes;
using Tup = RebindVariadic<std::tuple, Seq>::Type;
template<template<class> class META>
using Apply = ElmTypes<Seq>::template Apply<META>;
template<template<typename...> class O>
using Rebind = RebindVariadic<O, Seq>::Type;
template<template<class> class PRED>
using AndAll = ElmTypes<Apply<PRED>>::template Rebind<std::__and_>;
template<template<class> class PRED>
using OrAll = ElmTypes<Apply<PRED>>::template Rebind<std::__or_>;
};
namespace { // rebinding helper to create std::tuple from a type sequence
template<typename SEQ>
struct BuildTupleType
: std::false_type
{ };
template<typename...TYPES>
struct BuildTupleType<Types<TYPES...>>
{
using Type = std::tuple<TYPES...>;
};
template<class H, typename TAIL>
struct BuildTupleType<Node<H, TAIL>>
{
using Seq = Types<Node<H,TAIL>>::Seq;
using Type = BuildTupleType<Seq>::Type;
};
template<>
struct BuildTupleType<Nil>
{
using Type = std::tuple<>;
};
}
/** Build a `std::tuple` from types given as type sequence
* @remarks for Lumiera, we deliberately use a dedicated template `Types`
* to mark a type sequence of types as such. This allows to pass such a
* sequence as first-class citizen. The standard library often (ab)uses
* the std::tuple for this purpose, which is an understandable, yet
* inferior design choice. We should always favour dedicated types
* over clever re-use of existing types.
*/
template<typename TYPES>
using Tuple = BuildTupleType<TYPES>::Type;
using std::tuple_size;
using std::tuple_element;
/** match and rebind the type sequence from a tuple */
template<typename...TYPES>
struct RebindTupleTypes
{
using Seq = Types<TYPES...>::Seq;
using List = Seq::List;
};
template<typename...TYPES>
struct RebindTupleTypes<std::tuple<TYPES...>>
{
using Seq = Types<TYPES...>::Seq;
using List = Seq::List;
};
/**
* Extensible Adapter to construct a distinct tuple from some arbitrary source type.
* This includes the possibility to re-map elements or element positions.
* @tparam TYPES sequence of types to use for the tuple
* @tparam _ElmMapper_ a _template_ to extract each
* constructor argument from the source value.
* On invocation, we'll pick up the source type from the actual ctor argument,
* and then invoke this helper template iteratively for each component of the
* tuple, passing as template arguments
* - the source type, as picked up from the constructor
* - the target tuple type, i.e. `Tuple<TYPES>`
* - the actual index position of the tuple element
* to be initialised through this concrete instantiation.
* @remarks this design has several extension points. Pretty much any conceivable
* initialisation logic can be embodied in the `_ElmMapper_` template.
* Required is that the concrete instance is _constructible_ from the source type
* and _convertible_ to the individual member type of the target tuple it is
* invoked for. Source data _must_ be taken by-value, from the ctor argument.
* @note based on this mechanics, a generic _element extractor_ may be built,
* selecting a (partial) specialisation based on the source type given.
* @see ExtractArg
*/
template< typename TYPES
, template<class,class, size_t> class _ElmMapper_
>
struct TupleConstructor
: Tuple<TYPES>
{
/** meta-sequence to drive instantiation of the ElmMapper */
using SequenceIterator = BuildIdxIter<TYPES>::Ascending;
template<size_t idx, class SRC>
static auto
mapElm (SRC&& init) ///< initialise an instance of the element-mapper
{
return _ElmMapper_<std::decay_t<SRC>
,Tuple<TYPES>
, idx
>{std::forward<SRC> (init)};
}
protected:
template<class SRC, size_t...idx>
TupleConstructor (SRC&& initVals, IndexSeq<idx...>)
: Tuple<TYPES> {mapElm<idx> (std::forward<SRC>(initVals)) ...}
{ }
public:
template<class SRC>
TupleConstructor (SRC&& values)
: TupleConstructor (std::forward<SRC>(values), SequenceIterator())
{ }
};
/**
* Generic converter to somehow extract values from the "source"
* type to fill and initialise a tuple of given target type.
* @note to be specialised. The concrete specialisation is
* assumed to provide a _member template_ `Access<size_t>`,
* which in turn picks and converts the value for the n-th
* tuple element.
*/
template<class SRC, class TAR>
struct ElementExtractor;
template<class SRC, class TAR, size_t i>
using ExtractArg = ElementExtractor<SRC, TAR>::template Access<i>;
/**
* convenience shortcut to build a tuple from some suitable source data.
* For this to work, there needs to be a partial specialisation for
* (\ref ElementExtractor) to deal with the concrete source type given.
* @note we provide such a specialisation for `Record<GenNode>`, which
* allows us to fill an (argument) tuple from a sequence of generic
* data values, with run-time type compatibility check.
* @see tuple-record-init.hpp
*/
template<typename TYPES, class SRC>
Tuple<TYPES>
buildTuple (SRC&& values)
{
return TupleConstructor<TYPES, ExtractArg>{std::forward<SRC> (values)};
}
/**
* Decorating a tuple type with auxiliary data access operations.
* This helper template builds up a subclass of the given TUP (base) type
* (which is assumed to be a Tuple or at least need to be copy constructible
* from `Tuple<TYPES>` ). The purpose is to use the Tuple as storage record, but
* to add a layer of access functions, which in turn might rely on the exact
* type of the individual elements within the Tuple. To achieve this, for each
* type within the Tuple, the TUP type is decorated with an instance of the
* template passed in as template template parameter _X_. Each of these
* decorating instances is provided with an index number, allowing to
* access "his" specific element within the underlying tuple.
*
* The decorating template _X_ need to take its own base class as template
* parameter. Typically, operations on _X_ will be defined in a recursive fashion,
* calling down into this templated base class. To support this, an instantiation
* of _X_ with the empty type sequence is generated for detecting recursion end
* (built as innermost decorator, i.e. the immediate subclass of TUP)
*/
template
< template<class,class,class, uint> class _X_ ///< user provided template<Type, Base, TupleType, arg-idx>
, typename TYPES ///< Sequence of types to use within the Accessor
, class TUP =Tuple<TYPES> ///< the tuple type to build on
, uint i = 0 ///< tuple element index counter
>
class BuildTupleAccessor
{
// prepare recursion...
using Head = Split<TYPES>::Head;
using Tail = Split<TYPES>::Tail;
using NextBuilder = BuildTupleAccessor<_X_, Tail,TUP, i+1>;
using NextAccessor = NextBuilder::Product;
public:
/** type of the product created by this template.
* Will be a subclass of TUP */
using Product = _X_< Head // the type to use for this accessor
, NextAccessor // the base type to inherit from
, TUP // the tuple type we build upon
, i // current element index
>;
};
template
< template<class,class,class, uint> class _X_
, class TUP
, uint i
>
class BuildTupleAccessor< _X_, Types<>, TUP, i>
{
public:
using Product = _X_<Nil, TUP, TUP, i>; // Note: i == tuple size
};
/**
* Helper to dump tuple contents.
* Defined to act as "Accessor" for BuildTupleAccessor, this helper template
* allows to create a recursive operation to invoke string conversion on
* all elements within any given tuple.
*/
template
< typename TY
, class BASE
, class TUP
, uint idx
>
struct TupleElementDisplayer
: BASE
{
using BASE::BASE;
std::string
dump (std::string const& prefix ="(") const
{
return BASE::dump (prefix + util::toString(std::get<idx>(*this))+",");
}
};
template<class TUP, uint n>
struct TupleElementDisplayer<Nil, TUP, TUP, n>
: TUP
{
TupleElementDisplayer (TUP const& tup)
: TUP(tup)
{ }
std::string
dump (std::string const& prefix ="(") const
{
if (1 < prefix.length())
// remove the trailing comma
return prefix.substr (0, prefix.length()-1) +")";
else
return prefix+")";
}
};
/**
* convenience function to dump a given tuple's contents.
* Using the BuildTupleAccessor, we layer a stack of Instantiations of
* the TupleElementDisplayer temporarily on top of the given tuple,
* just to invoke a recursive call chain through these layers
* and get a string representation of each element in the
* tuple.
*/
template<typename...TYPES>
inline std::string
dump (std::tuple<TYPES...> const& tuple)
{
using BuildAccessor = BuildTupleAccessor<TupleElementDisplayer, Types<TYPES...>>;
using Displayer = BuildAccessor::Product ;
return static_cast<Displayer const&> (tuple)
.dump();
}
}} // namespace lib::meta
// add a specialisation to enable tuple string conversion
namespace util {
template<typename...TYPES>
struct StringConv<std::tuple<TYPES...>>
{
static std::string
invoke (std::tuple<TYPES...> const& tuple) noexcept
try {
return "«"+typeStr(tuple)
+ "»──" + lib::meta::dump (tuple);
}
catch(...) { return FAILURE_INDICATOR; }
};
} // namespace util
#endif /*LIB_META_TUPLE_ACCESSOR_H*/

View file

@ -0,0 +1,607 @@
/*
TUPLE-CONCEPT.hpp - metaprogramming support for tuple-like data types
Copyright (C)
2025, Hermann Vosseler <Ichthyostega@web.de>
  **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-concept.hpp
** Metaprogramming with tuples-of-types and the `std::tuple` record.
** This header complements typelist.hpp and provides a bridge from type sequences
** to the tuple type provided by the standard library, including traits and
** helpers to build tuple types from metaprogramming and to pretty-print tuples.
**
** Notably, a `concept tuple_like` is provided here, which is satisfied for any type
** in compliance with the »tuple protocol«. This solution reaches beyond the scope
** of the C++ standard — and in fact opposes the decision of the C++ Committee
** _not to provide such a concept_, since it can not be defined in full genericity
** for any type that can be handled by the _structural bindings of the C++ language.
**
** Since the Lumiera support library is part of a specific application framework, and
** does not intend to be a general purpose library, we prefer a partially complete
** definition that allows us to subsume and handle custom types under a »Tuple« concept.
** @note Unfortunately the C++ committee did not only decline to provide a tuple_like concept,
** rather they choose to lock-down essential parts of the accompanying library to their
** internal and hard-wired selection of what is considered "tuple-like". This forces us
** to redefine and replace also the crucial function `std::apply`, so that a generic
** function can be applied to any _tuple-like_ entity.
** @see TupleHelper_test
** @see tuple-helper.hpp extended tuple support functions
**
*/
#ifndef LIB_META_TUPLE_CONCEPT_H
#define LIB_META_TUPLE_CONCEPT_H
#include "lib/meta/typelist.hpp"
#include "lib/meta/typelist-util.hpp"
#include "lib/meta/typeseq-util.hpp"
#include "lib/meta/variadic-helper.hpp"
#include "lib/meta/util.hpp"
#include <tuple>
#include <utility>
#include <functional>
namespace util { // forward declaration
template<typename TY>
std::string
toString (TY const& val) noexcept;
}
namespace lib {
namespace meta {
/** trait to detect tuple types */
template<typename T>
struct is_Tuple
: std::false_type
{ };
template<typename...TYPES>
struct is_Tuple<std::tuple<TYPES...>>
: std::true_type
{ };
template<typename...TYPES>
struct is_Tuple<const std::tuple<TYPES...>>
: std::true_type
{ };
using std::remove_cv_t;
using std::is_reference_v;
using std::remove_reference_t;
/** @internal building-block: a type supporting the `tuple_size` metafunction */
template<class TUP>
concept tuple_sized = requires
{
{ std::tuple_size<TUP>::value } -> std::convertible_to<size_t>;
};
/** @internal building-block: a type where elements can be accessed through a `get` friend function */
template<class TUP, std::size_t idx>
concept tuple_adl_accessible = requires(TUP tup)
{
typename std::tuple_element_t<idx, TUP>;
{ get<idx>(tup) } -> std::convertible_to<std::tuple_element_t<idx, TUP>&>;
};
/** @internal building-block: a type where elements can be accessed through a `get` member function */
template<class TUP, std::size_t idx>
concept tuple_mem_accessible = requires(TUP tup)
{
typename std::tuple_element_t<idx, TUP>;
{ tup.template get<idx>() } -> std::convertible_to<std::tuple_element_t<idx, TUP>&>;
};
template<class TUP, std::size_t idx>
concept tuple_element_accessible = tuple_mem_accessible<TUP,idx> or tuple_adl_accessible<TUP,idx>;
template<class TUP>
concept tuple_accessible =
tuple_sized<TUP> and
WithIdxSeq<std::tuple_size_v<TUP>>::andAll([](auto idx)
{
return tuple_element_accessible<TUP,idx>;
});
/**
* Concept to mark any type compliant to the »tuple protocol«
*/
template<class TUP>
concept tuple_like = not is_reference_v<TUP>
and tuple_sized<remove_cv_t<TUP>>
and tuple_accessible<remove_cv_t<TUP>>;
/**
* Helper for abstracted / unified access to member elements of any _tuple-like_
* @remark preferably uses a `get<i>` member function, falling back to a
* free function `get`, which is found by ADL.
*/
template<std::size_t idx, class TUP>
requires(tuple_like<std::remove_reference_t<TUP>>)
decltype(auto)
getElm (TUP&& tup)
{
using Tup = std::remove_reference_t<TUP>;
static_assert (0 < std::tuple_size_v<Tup>);
if constexpr (tuple_mem_accessible<Tup,0>)
{
if constexpr (std::is_reference_v<TUP>)
return tup.template get<idx>();
else
{ // return value copy when tuple given as RValue
using Elm = std::tuple_element_t<idx, TUP>;
Elm elm(tup.template get<idx>());
return elm;
}
}
else
{ // ▽▽▽ ADL
using std::get;
return get<idx> (std::forward<TUP> (tup));
}
}
namespace { // apply to tuple-like : helpers...
/** @internal invocation helper similar to C++17 — but preferring a custom `get` impl */
template<typename FUN, typename TUP, size_t...Idx>
constexpr decltype(auto)
__unpack_and_apply (FUN&& f, TUP&& tup, std::index_sequence<Idx...>)
{
return std::invoke (std::forward<FUN> (f)
,getElm<Idx> (std::forward<TUP>(tup))...
);
}
/** @internal invoke a metafunction with \a FUN and all element types from the _tuple-like_ \a TUP */
template<template<typename...> class META, class FUN, class TUP>
struct _InvokeMetafunTup
{
using Tupl = std::decay_t<TUP>;
using Elms = ElmTypes<Tupl>::Seq;
using Args = Prepend<FUN, Elms>::Seq;
using Type = RebindVariadic<META, Args>::Type;
};
template<template<typename...> class META, class FUN, class TUP>
struct _InvokeMetafunTup<META, FUN, TUP&>
{
using Tupl = std::decay_t<TUP>;
using Elms = typename ElmTypes<Tupl>::template Apply<std::add_lvalue_reference_t>;
using Args = Prepend<FUN, Elms>::Seq;
using Type = RebindVariadic<META, Args>::Type;
};
template<class FUN, class TUP>
inline constexpr bool can_nothrow_invoke_tup = _InvokeMetafunTup<std::is_nothrow_invocable,FUN,TUP>::Type::value;
}
/**
* Replacement for `std::apply` — yet applicable to _tuple-like custom types_.
* For unclear reasons, the standard chooses to reject such custom types, and
* only allows a fixed set of explicitly defined facilities from the Stdlib
* (tuple, pair, array, and some ranges stuff).
* @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<class FUN, class TUP> requires(tuple_like<remove_reference_t<TUP>>)
constexpr decltype(auto)
apply (FUN&& f, TUP&& tup) noexcept (can_nothrow_invoke_tup<FUN,TUP> )
{
using Indices = std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<TUP>>>;
return __unpack_and_apply (std::forward<FUN> (f)
,std::forward<TUP> (tup)
,Indices{}
);
}
/**
* Tuple iteration: perform some arbitrary operation on each element of a tuple.
* @note the given functor must be generic, since each position of the tuple
* may hold a data element of different type.
* @remark credits to David Vandevoorde (member of C++ committee) for using
* std::apply to unpack the tuple's contents into an argument pack and
* then employ a fold expression with the comma operator.
*/
template<class TUP, class FUN> requires(tuple_like<remove_reference_t<TUP>>)
constexpr void
forEach (TUP&& tuple, FUN fun)
{
lib::meta::apply ([&fun]<typename...ELMS>(ELMS&&... elms)
{
(fun (std::forward<ELMS>(elms)), ...);
}
,std::forward<TUP> (tuple));
}
/**
* Apply some arbitrary function onto all elements of a tuple.
* @return a new tuple constructed from the results of this function
* @note the functor must be generic and able to work with all element types
* @warning pay attention to references; the function can take arguments by-ref
* and manipulate them (side-effect), and it may return references,
* which will be placed as such into the result tuple; furthermore,
* the argument tuple can also be taken as reference...
* @remark The tuple constructor invocation is preceded by a regular argument evaluation,
* which has unspecified evaluation order (even in C++17); _no assumptions_ can be
* made regarding the order the functor will see the source tuple elements.
* Notably this differs from #forEach, where a fold-expression with comma-operator
* is used, which is guaranteed to evaluate from left to right.
*/
template<class TUP, class FUN> requires(tuple_like<remove_reference_t<TUP>>)
constexpr auto
mapEach (TUP&& tuple, FUN fun)
{
return lib::meta::apply ([&fun]<typename...ELMS>(ELMS&&... elms)
{ //..construct the type explicitly (make_tuple would decay fun result types)
using Tuple = std::tuple<decltype(fun (std::forward<ELMS>(elms))) ...>;
return Tuple (fun (std::forward<ELMS>(elms)) ...);
}
,std::forward<TUP> (tuple));
}
/**
* Specialisation of variadic access for any tuple-like
* @see variadic-helper.hpp
*/
template<tuple_like TUP>
struct ElmTypes<TUP>
{
template<typename>
struct Extract;
template<size_t...idx>
struct Extract<std::index_sequence<idx...>>
{
using ElmTypes = Types<std::tuple_element_t<idx,TUP> ...>;
};
static constexpr size_t SIZ = std::tuple_size_v<TUP>;
using Idx = std::make_index_sequence<SIZ>;
using Seq = Extract<Idx>::ElmTypes;
using Tup = RebindVariadic<std::tuple, Seq>::Type;
template<template<class> class META>
using Apply = ElmTypes<Seq>::template Apply<META>;
template<template<typename...> class O>
using Rebind = RebindVariadic<O, Seq>::Type;
template<template<class> class PRED>
using AndAll = ElmTypes<Apply<PRED>>::template Rebind<std::__and_>;
template<template<class> class PRED>
using OrAll = ElmTypes<Apply<PRED>>::template Rebind<std::__or_>;
};
namespace { // rebinding helper to create std::tuple from a type sequence
template<typename SEQ>
struct BuildTupleType
: std::false_type
{ };
template<typename...TYPES>
struct BuildTupleType<Types<TYPES...>>
{
using Type = std::tuple<TYPES...>;
};
template<class H, typename TAIL>
struct BuildTupleType<Node<H, TAIL>>
{
using Seq = Types<Node<H,TAIL>>::Seq;
using Type = BuildTupleType<Seq>::Type;
};
template<>
struct BuildTupleType<Nil>
{
using Type = std::tuple<>;
};
}
/** Build a `std::tuple` from types given as type sequence
* @remarks for Lumiera, we deliberately use a dedicated template `Types`
* to mark a type sequence of types as such. This allows to pass such a
* sequence as first-class citizen. The standard library often (ab)uses
* the std::tuple for this purpose, which is an understandable, yet
* inferior design choice. We should always favour dedicated types
* over clever re-use of existing types.
*/
template<typename TYPES>
using Tuple = BuildTupleType<TYPES>::Type;
using std::tuple_size;
using std::tuple_element;
/** match and rebind the type sequence from a tuple */
template<typename...TYPES>
struct RebindTupleTypes
{
using Seq = Types<TYPES...>::Seq;
using List = Seq::List;
};
template<typename...TYPES>
struct RebindTupleTypes<std::tuple<TYPES...>>
{
using Seq = Types<TYPES...>::Seq;
using List = Seq::List;
};
/**
* Extensible Adapter to construct a distinct tuple from some arbitrary source type.
* This includes the possibility to re-map elements or element positions.
* @tparam TYPES sequence of types to use for the tuple
* @tparam _ElmMapper_ a _template_ to extract each
* constructor argument from the source value.
* On invocation, we'll pick up the source type from the actual ctor argument,
* and then invoke this helper template iteratively for each component of the
* tuple, passing as template arguments
* - the source type, as picked up from the constructor
* - the target tuple type, i.e. `Tuple<TYPES>`
* - the actual index position of the tuple element
* to be initialised through this concrete instantiation.
* @remarks this design has several extension points. Pretty much any conceivable
* initialisation logic can be embodied in the `_ElmMapper_` template.
* Required is that the concrete instance is _constructible_ from the source type
* and _convertible_ to the individual member type of the target tuple it is
* invoked for. Source data _must_ be taken by-value, from the ctor argument.
* @note based on this mechanics, a generic _element extractor_ may be built,
* selecting a (partial) specialisation based on the source type given.
* @see ExtractArg
*/
template< typename TYPES
, template<class,class, size_t> class _ElmMapper_
>
struct TupleConstructor
: Tuple<TYPES>
{
/** meta-sequence to drive instantiation of the ElmMapper */
using SequenceIterator = BuildIdxIter<TYPES>::Ascending;
template<size_t idx, class SRC>
static auto
mapElm (SRC&& init) ///< initialise an instance of the element-mapper
{
return _ElmMapper_<std::decay_t<SRC>
,Tuple<TYPES>
, idx
>{std::forward<SRC> (init)};
}
protected:
template<class SRC, size_t...idx>
TupleConstructor (SRC&& initVals, IndexSeq<idx...>)
: Tuple<TYPES> {mapElm<idx> (std::forward<SRC>(initVals)) ...}
{ }
public:
template<class SRC>
TupleConstructor (SRC&& values)
: TupleConstructor (std::forward<SRC>(values), SequenceIterator())
{ }
};
/**
* Generic converter to somehow extract values from the "source"
* type to fill and initialise a tuple of given target type.
* @note to be specialised. The concrete specialisation is
* assumed to provide a _member template_ `Access<size_t>`,
* which in turn picks and converts the value for the n-th
* tuple element.
*/
template<class SRC, class TAR>
struct ElementExtractor;
template<class SRC, class TAR, size_t i>
using ExtractArg = ElementExtractor<SRC, TAR>::template Access<i>;
/**
* convenience shortcut to build a tuple from some suitable source data.
* For this to work, there needs to be a partial specialisation for
* (\ref ElementExtractor) to deal with the concrete source type given.
* @note we provide such a specialisation for `Record<GenNode>`, which
* allows us to fill an (argument) tuple from a sequence of generic
* data values, with run-time type compatibility check.
* @see tuple-record-init.hpp
*/
template<typename TYPES, class SRC>
Tuple<TYPES>
buildTuple (SRC&& values)
{
return TupleConstructor<TYPES, ExtractArg>{std::forward<SRC> (values)};
}
/**
* Decorating a tuple type with auxiliary data access operations.
* This helper template builds up a subclass of the given TUP (base) type
* (which is assumed to be a Tuple or at least need to be copy constructible
* from `Tuple<TYPES>` ). The purpose is to use the Tuple as storage record, but
* to add a layer of access functions, which in turn might rely on the exact
* type of the individual elements within the Tuple. To achieve this, for each
* type within the Tuple, the TUP type is decorated with an instance of the
* template passed in as template template parameter _X_. Each of these
* decorating instances is provided with an index number, allowing to
* access "his" specific element within the underlying tuple.
*
* The decorating template _X_ need to take its own base class as template
* parameter. Typically, operations on _X_ will be defined in a recursive fashion,
* calling down into this templated base class. To support this, an instantiation
* of _X_ with the empty type sequence is generated for detecting recursion end
* (built as innermost decorator, i.e. the immediate subclass of TUP)
*/
template
< template<class,class,class, uint> class _X_ ///< user provided template<Type, Base, TupleType, arg-idx>
, typename TYPES ///< Sequence of types to use within the Accessor
, class TUP =Tuple<TYPES> ///< the tuple type to build on
, uint i = 0 ///< tuple element index counter
>
class BuildTupleAccessor
{
// prepare recursion...
using Head = Split<TYPES>::Head;
using Tail = Split<TYPES>::Tail;
using NextBuilder = BuildTupleAccessor<_X_, Tail,TUP, i+1>;
using NextAccessor = NextBuilder::Product;
public:
/** type of the product created by this template.
* Will be a subclass of TUP */
using Product = _X_< Head // the type to use for this accessor
, NextAccessor // the base type to inherit from
, TUP // the tuple type we build upon
, i // current element index
>;
};
template
< template<class,class,class, uint> class _X_
, class TUP
, uint i
>
class BuildTupleAccessor< _X_, Types<>, TUP, i>
{
public:
using Product = _X_<Nil, TUP, TUP, i>; // Note: i == tuple size
};
/**
* Helper to dump tuple contents.
* Defined to act as "Accessor" for BuildTupleAccessor, this helper template
* allows to create a recursive operation to invoke string conversion on
* all elements within any given tuple.
*/
template
< typename TY
, class BASE
, class TUP
, uint idx
>
struct TupleElementDisplayer
: BASE
{
using BASE::BASE;
std::string
dump (std::string const& prefix ="(") const
{
return BASE::dump (prefix + util::toString(std::get<idx>(*this))+",");
}
};
template<class TUP, uint n>
struct TupleElementDisplayer<Nil, TUP, TUP, n>
: TUP
{
TupleElementDisplayer (TUP const& tup)
: TUP(tup)
{ }
std::string
dump (std::string const& prefix ="(") const
{
if (1 < prefix.length())
// remove the trailing comma
return prefix.substr (0, prefix.length()-1) +")";
else
return prefix+")";
}
};
/**
* convenience function to dump a given tuple's contents.
* Using the BuildTupleAccessor, we layer a stack of Instantiations of
* the TupleElementDisplayer temporarily on top of the given tuple,
* just to invoke a recursive call chain through these layers
* and get a string representation of each element in the
* tuple.
*/
template<typename...TYPES>
inline std::string
dump (std::tuple<TYPES...> const& tuple)
{
using BuildAccessor = BuildTupleAccessor<TupleElementDisplayer, Types<TYPES...>>;
using Displayer = BuildAccessor::Product ;
return static_cast<Displayer const&> (tuple)
.dump();
}
}} // namespace lib::meta
// add a specialisation to enable tuple string conversion
namespace util {
template<typename...TYPES>
struct StringConv<std::tuple<TYPES...>>
{
static std::string
invoke (std::tuple<TYPES...> const& tuple) noexcept
try {
return "«"+typeStr(tuple)
+ "»──" + lib::meta::dump (tuple);
}
catch(...) { return FAILURE_INDICATOR; }
};
} // namespace util
#endif /*LIB_META_TUPLE_CONCEPT_H*/

View file

@ -2,7 +2,7 @@
TUPLE-HELPER.hpp - metaprogramming utilities for type and data tuples
Copyright (C)
2016, Hermann Vosseler <Ichthyostega@web.de>
2016,2025 Hermann Vosseler <Ichthyostega@web.de>
  **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
@ -18,11 +18,16 @@
** to the tuple type provided by the standard library, including traits and
** helpers to build tuple types from metaprogramming and to pretty-print tuples.
**
** Notably, a `concept tuple_like` is provided, which is satisfied for any type in compliance
** with the »tuple protocol«. Together with a [generic accessor][\ref lib::meta::getElm],
** this allows to handle all _tuple-like_ types uniformly.
** @note Due to an unfortunate limitation of the standard, we're forced to provide our own alternative
** implementation to replace `std::apply`, so that a function can be applied to any _tuple-like_
** Notably the handling of tuples is based on a **Concept** `tuple_like`,
** that is satisfied for any type in compliance with the »tuple protocol«.
** Together with a [generic accessor][\ref lib::meta::getElm], this allows
** to handle all _tuple-like_ types uniformly.
** This solution reaches beyond the scope of the C++ standard, which declines
** to define such a concept and rather provides an "exposition-only" concept
** that is limited to a hard-wired selection of library types, yet does not
** cover custom user provided types able to participate in _structured bindings_.
** @note Due to that unfortunate limitation of the standard, we're forced to
** provide our own alternative implementation of `std::apply`.
**
** Furthermore, a generic iteration construct is provided, to instantiate
** a generic Lambda for each element of a given tuple, which allows to write

View file

@ -37,6 +37,12 @@
** creates an *Extension Point*: when some payload type requires special
** treatment, an explicit specialisation to this rebinding trait may be
** injected alongside with the definition of the payload type.
** @warning A special twist may happen when processing a function in an iterator (pipeline).
** In this special case, `ValueTypeBinding` should _not_ be used, since the function
** might yield again a nested iterator; yet we want to treat that iterator as a value
** in that special case, and not look into the iterator or retrieve results from it.
** Notably this situation applies to any kind of transforming iterator.
** lib::meta::RefTraits should be used in such cases.
**
** The CommonResultYield type rebinding helper allows to reconcile several
** essentially compatible result types; it is used in iterator pipelines,