LUMIERA.clone/src/lib/hetero-data.hpp
Ichthyostega 20f3252892 Upgrade: down with typename!!
Yet another chainsaw massacre.

One of the most obnoxious annoyances with C++ metaprogramming
is the need to insert `typename` and `template` qualifiers into
most definitions, to help the compiler to cope with the syntax,
which is not context-free.

The recent standards adds several clarifications, so that most
of these qualifiers are redundant now, at least at places where
it is unambiguously clear that only a type can be given.

GCC already supports most of these relaxing rules
(Clang unfortunately lags way behind with support of newer language features...)
2025-07-06 01:19:08 +02:00

466 lines
18 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
HETERO-DATA.hpp - handle chain of heterogeneous data blocks
Copyright (C)
2024, 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 hetero-data.hpp
** Maintain a chained sequence of heterogeneous data blocks without allocation.
** This building block for low-level memory management allows to build up a collection
** of entirely arbitrary data placed into existing and possibly distributed storage.
** The safety of storage and lifetime must be ensured by other means, since data access
** proceeds without further bound checks. However, a type-safe compile-time overlay of
** accessor marker types is provided, allowing to integrate such a storage layout into
** an overall memory safe arrangement.
**
** A usage scenario would be gradually to build up an assortment of data elements directly
** in local automatic storage within an elaborate recursive call stack unfolding recursively.
** Notably the accessor marker types can be assembled independently from the provision of
** actual storage, as the connection between accessor and actual storage address is
** _established late,_ on actual _data access._ Obviously, data access in such an arrangement
** requires traversal in several steps, which, on the other hand, can be justified by a good
** cache locality of recently used stack frames, thereby avoiding heap allocations altogether.
**
** # Usage
** @warning it is essential to understand where actual storage resides!
**
** A HeteroData chain is built-up gradually, starting with a front-block
** - the front-block is usually placed at an _anchor location_ and populated with data
** - retrieve a _chain constructor type_ from the _type_ of the front-block,
** i.e `HeteroData<D1,D2,...>::Chain<D21,...>`
** - use this chain constructor to create a follow-up data block elsewhere
** - need to link this data block explicitly into the front
** - get _accessor types_ from the _chain constructor_
** - use these to work with individual data elements _through the front-block._
**
**\par example of typical usage
** \code
** using Front = lib::HeteroData<uint,double>;
** auto h1 = Front::build (1,2.3);
** using Cons1 = Front::Chain<bool,string>;
** auto b2 = Cons1::build (true, "Ψ");
** b2.linkInto(h1);
** auto& [d1,d2,d3,d4] = Cons1::recast(h1);
** CHECK (d1 == 1);
** CHECK (d2 == 2.3);
** CHECK (d3 == true);
** CHECK (d4 == "Ψ");
** Cons1::AccessorFor<string> get4;
** CHECK (get4(h1) == "Ψ");
** \endcode
**
** @todo WIP-WIP this is the draft of a design sketch regarding the render node network,
** which seems to be still pretty much in flux as of 12/2024
** @see HeteroData_test
** @see steam::engine::TurnoutSystem (use case)
**
*/
#ifndef LIB_HETERO_DATA_H
#define LIB_HETERO_DATA_H
#include "lib/error.hpp"
#include "lib/nocopy.hpp"
#include "lib/meta/typelist.hpp"
#include "lib/meta/typelist-manip.hpp"
#include "lib/meta/typelist-util.hpp"
#include "lib/meta/typeseq-util.hpp"
#include <utility>
#include <tuple>
namespace lib {
/**
* A setup with chained data tuples residing in distributed storage.
* A HeteroData-chain is started from a front-end block and can later be
* extended by a linked list of further data blocks allocated elsewhere.
* @warning this is a low-level memory layout without storage management.
* @see HeteroData_test
*/
template<typename...DATA>
class HeteroData;
/** linked list of StorageFrame elements */
struct StorageLoc
: util::NonCopyable
{
StorageLoc* next{nullptr};
};
/** individual storage frame in a chain, holding a data tuple */
template<size_t seg, typename...DATA>
struct StorageFrame
: protected StorageLoc
, std::tuple<DATA...>
{
using Tuple = std::tuple<DATA...>;
using Tuple::tuple;
template<typename SPEC>
void linkInto (HeteroData<SPEC>&);
template<typename SPEC>
void detachFrom (HeteroData<SPEC>&);
template<size_t slot> auto& get() noexcept { return std::get<slot>(*this); }
template<typename X> auto& get() noexcept { return std::get<X>(*this); }
};
/**
* @internal implementation specialisation to manage a sublist of StorageFrame elements
* @tparam seg a type tag to mark the position of StorageFrame elements
* @tparam DATA tuple element types residing in the first segment
* @tparam TAIL recursive Loki-style type list to describe the rest of the chain
*/
template<size_t seg, typename...DATA, class TAIL>
class HeteroData<meta::Node<StorageFrame<seg, DATA...>,TAIL>>
: StorageFrame<seg, DATA...>
{
using _Self = HeteroData;
using _Tail = HeteroData<TAIL>;
using Tuple = std::tuple<DATA...>;
using Frame = StorageFrame<seg, DATA...>;
static constexpr size_t localSiz = sizeof...(DATA);
template<size_t slot>
static constexpr bool isLocal = slot < localSiz;
template<size_t slot>
using PickType = std::conditional_t<isLocal<slot>, std::tuple_element<slot,Tuple>
, typename _Tail::template PickType<slot-localSiz>>;
// need to use this helper to prevent eager evaluation on Elm_t<i>
_Tail&
accessTail()
{
if (Frame::next == nullptr) // Halt system by breaking noexcept
throw lumiera::error::Fatal{"HeteroData storage logic broken: follow-up extent not(yet) allocated"};
return * reinterpret_cast<_Tail*> (Frame::next);
}
template<typename...SPEC>
static _Self&
recast (HeteroData<SPEC...>& frontChain)
{
return reinterpret_cast<_Self&> (frontChain);
}
template<typename...SPEC>
static _Self const&
recast (HeteroData<SPEC...> const& frontChain)
{
return reinterpret_cast<_Self const&> (frontChain);
}
template<typename...XX>
friend class HeteroData; ///< allow chained types to use recursive type definitions
using Frame::Frame; ///< data elements shall be populated through the builder front-ends
public:
HeteroData() = default;
static constexpr size_t
size()
{
return localSiz + _Tail::size();
}
/** access type to reside in the given slot of the _complete chain_ */
template<size_t slot>
using Elm_t = PickType<slot>::type;
/** access data elements within _complete chain_ by index pos */
template<size_t slot>
Elm_t<slot>&
get() noexcept
{
static_assert (slot < size(), "HeteroData access index beyond defined data");
if constexpr (slot < localSiz)
return std::get<slot> (*this);
else
return accessTail().template get<slot-localSiz>();
}
template<size_t slot>
Elm_t<slot> const&
get() const noexcept
{
return const_cast<HeteroData*>(this)->get<slot>();
}
/**
* Accessor-functor to get at the data residing within some tuple element.
* Using the enclosing typed scope to ensure safe storage access
* @tparam slot number of the data element, counting from zero over the full chain
* @note this functor holds no data, but shall be applied to some existing HeteroData.
*/
template<size_t slot>
struct Accessor
{
using Type = Elm_t<slot>;
template<class SPEC>
static Type&
retrieveData (HeteroData<SPEC>& frontEnd)
{
auto& fullChain = _Self::recast (frontEnd);
return fullChain.template get<slot>();
}
template<typename HH>
Type& operator() (HH& frontEnd) const { return Accessor::retrieveData (frontEnd); }
};
/**
* Constructor-functor to build an extra data segment, which can then be linked to the chain.
* @tparam VALS data types to use in the extra storage tuple
* @note Using this functor is the only safe path to create and add new data blocks.
* Each such data block can be linked in once, and only if the base chain matches
* the structure embedded into the type of the enclosing scope.
* - storage frames can be default constructed, but not copied / moved thereafter
* - the #build() function can be used to create the block and init the data
* - after creating a frame, it must be explicitly linked in by invoking NewFrame::linkInto()
* - the #recast() function will re-interpret _any_ `HeteroData&` into the storage structure
* which can be expected after building the extension frame (use with care!)
* - the nested template ChainExtent is a follow-up constructor-functor to add a further block
* - the nested template Accessor shall be used for any type-save access to data values
* - if all types are distinct, the Accessor can also be selected by-type
*/
template<typename...VALS>
struct Chain
{
using Segments = meta::Node<Frame,TAIL>; // ◁———this type describes current chain structure
using NewFrame = StorageFrame<meta::count<Segments>::value, VALS...>;
using ChainType = HeteroData<typename meta::Append<Segments,NewFrame>::List>;
// ...and this would be the extended chain structure
template<typename...INIT>
static NewFrame
build (INIT&& ...initArgs)
{
return {initArgs ...}; // Note: NewFrame is non-copyable
}
template<typename...INIT>
static NewFrame&
emplace (void* storage, INIT&& ...initArgs) ///< placement-new flavour of the builder notation
{
return * new(storage) NewFrame{initArgs ...};
}
template<class HET>
static auto&
recast (HET& frontChain)
{
return ChainType::recast (frontChain);
}
template<typename...XVALS>
using ChainExtent = ChainType::template Chain<XVALS...>;
template<size_t slot>
using Accessor = ChainType::template Accessor<_Self::size()+slot>;
template<typename X>
using AccessorFor = Accessor<meta::indexOfType<X,VALS...>()>;
};
};
/**
* @internal implementation specialisation to mark the end of a chain
*/
template<>
class HeteroData<meta::Nil>
{
public:
static size_t constexpr size() { return 0; }
template<size_t>
using Elm_t = void;
template<size_t>
using PickType = void;
};
/*************************************************************************//**
* @remark this is the front-end for regular usage
* - create and populate with the #build operation
* - data access with the `get<i>` member function (inherited)
* - use `HeteroData<DATA...>::Chain<TY...>` to build follow-up segments
*/
template<typename...DATA>
class HeteroData
: public HeteroData<meta::Node<StorageFrame<0, DATA...>, meta::Nil>>
{
using _FrontBlock = HeteroData<meta::Node<StorageFrame<0, DATA...>, meta::Nil>>;
public:
using NewFrame = _FrontBlock::Frame;
using ChainType = _FrontBlock;
using _FrontBlock::_FrontBlock;
template<typename...INIT>
static HeteroData
build (INIT&& ...initArgs)
{
return {initArgs ...};
}
};
namespace {
/**
* @internal helper for safety-check when attaching segments.
* New segments are created by a constructor functor, guided by a
* type signature describing the complete chain. When attaching new
* segments, we can not verify that the base chain to extend does really
* match the presumed chain structure as encoded into the type (since this
* base chain does not store any meta data). But at least we can verify
* that the number of real segment-links matches the assumed structure.
* Notably it does not really matter what is stored in the base chain,
* as long as this segment count matches, because accessor functors
* generated by a `HeteroData::Chain` constructor will always address
* only their own (newly added) segment.
*/
inline StorageLoc*&
checkedTraversal (size_t segments, StorageLoc* last)
{
REQUIRE(last);
while (segments and last->next)
{
last = last->next;
--segments;
}
ASSERT (last->next == nullptr and segments == 1
,"Failure to attach new data segment to HeteroData: "
"assumed type structure does not match real connectivity, "
"end-of-chain encountered with %d type segment(s) remaining"
, segments);
return last->next;
}
/**
* @internal detach the HeteroData-chain at the link to the given chainBlock.
* This enables some additional data sanity, because the internal chain can
* thus be severed when an extension data block is known to go out of scope.
* If somehow a store accessor is used after that point, the system will
* be halted when attempting to navigate to the (now defunct) data block.
*/
inline void
checkedDetach (size_t segments, StorageLoc* seg, void* chainBlock)
{
REQUIRE(seg);
while (segments and seg->next)
if (segments == 1 and seg->next == chainBlock)
{
seg->next = nullptr;
return;
}
else
{
seg = seg->next;
--segments;
}
NOTREACHED ("Failure to detach a data segment from HeteroData: "
"assumed type structure does not match real connectivity");
}
}//(End)helper
/**
* Attach a new storage frame at the end of an existing HeteroData-chain.
* @tparam seg the number of the separate data segment, must match target
* @param prefixChain with `seg - 1` existing chained tuple-segments
* @remark The core function actually to extend a chain with a new segment,
* which should have been built using a suitable nested `HeteroData::Chain`
* constructor type. Further segments can be defined working from there,
* since each such constructor in turn has a member type `ChainExtension`
* @note Always use this strongly typed extension and access path, to prevent
* out-of-bounds memory access. The actual HeteroData stores no run time
* type information, and thus a force-cast is necessary internally to
* access the follow-up data tuple frames. The typing, and especially
* the `seg` template parameter used to mark each StorageFrame is
* the only guard-rail provided, and ensures safe data access.
*/
template<size_t seg, typename...DATA>
template<typename SPEC>
inline void
StorageFrame<seg,DATA...>::linkInto (HeteroData<SPEC>& prefixChain)
{
StorageLoc* firstSeg = reinterpret_cast<StorageLoc*> (&prefixChain);
StorageLoc*& lastLink = checkedTraversal (seg, firstSeg);
ENSURE (lastLink == nullptr);
lastLink = this;
}
/** cleanly detach this storage frame from the HeteroData prefix-chain. */
template<size_t seg, typename...DATA>
template<typename SPEC>
inline void
StorageFrame<seg,DATA...>::detachFrom (HeteroData<SPEC>& prefixChain)
{
StorageLoc* firstSeg = reinterpret_cast<StorageLoc*> (&prefixChain);
checkedDetach (seg, firstSeg, this);
}
}// namespace lib
namespace std { // Specialisation to support C++ »Tuple Protocol« and structured bindings.
/** determine compile-time fixed size of a HeteroData */
template<typename...DATA>
struct tuple_size<lib::HeteroData<DATA...> >
: integral_constant<size_t, lib::HeteroData<DATA...>::size()>
{ };
/** expose the type of the I-th element of a HeteroData chain */
template<size_t I, typename...DATA>
struct tuple_element<I, lib::HeteroData<DATA...> >
{
using type = lib::HeteroData<DATA...>::template Elm_t<I>;
};
template<size_t I>
struct tuple_element<I, lib::HeteroData<lib::meta::Nil> >
{
static_assert ("accessing element-type of an empty HeteroData block");
};
// Note: deliberately NOT providing a free get<i> function.
// Overload resolution would fail, since it attempts to instantiate std::get<i>(tuple) as a candidate,
// which triggers an assertion failure when using an index valid only for the full chain, not the base tuple
/** determine compile-time fixed size of a StorageFrame */
template<size_t seg, typename...DATA>
struct tuple_size<lib::StorageFrame<seg,DATA...> >
: tuple_size<typename lib::StorageFrame<seg,DATA...>::Tuple>
{ };
/** delegate to the type access of a StorageFrame's underlying tuple */
template<size_t I, size_t seg, typename...DATA>
struct tuple_element<I, lib::StorageFrame<seg,DATA...> >
: tuple_element<I, typename lib::StorageFrame<seg,DATA...>::Tuple>
{ };
// no need to define an overload for std::get<i>
// (other than a template specialisation, it will use base-type conversion to std::tuple on its argument;
}// namespace std
#endif /*LIB_HETERO_DATA_H*/