From ea627c1c7a673f7c2d278770a121ef54baa81f06 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Thu, 26 Feb 2026 23:25:07 +0100 Subject: [PATCH] BufferProvider: document the new design and use it for the prototype Explored further aspects of this new design; as an aside, I tried to build a Concept to describe the policy, which howerer turned out to be much more challenging than expected. Lastly, I could now complete the prototype of `BufferProxyAdaptor` --- src/lib/meta/prop-builder.hpp | 60 ++++++- src/steam/engine/buffhandle.hpp | 2 +- .../engine/output-proxy-provider-test.cpp | 24 ++- tests/library/meta/prop-builder-test.cpp | 123 ++++++++----- wiki/thinkPad.ichthyo.mm | 161 +++++++++++++++--- 5 files changed, 293 insertions(+), 77 deletions(-) diff --git a/src/lib/meta/prop-builder.hpp b/src/lib/meta/prop-builder.hpp index acbaf0abc..4482bb33a 100644 --- a/src/lib/meta/prop-builder.hpp +++ b/src/lib/meta/prop-builder.hpp @@ -14,6 +14,58 @@ /** @file prop-builder.hpp ** A builder for compile-time generation of data records with arbitrary named fields. + ** Generally speaking, a "record" in this context means just a struct or class with some + ** data fields, and can be easily defined to suit any needs. However, such a class definition + ** always incurs some _syntactical overhead_ — which is fine for spelling out some type in + ** general, but can be problematic when the data record is only used once, locally and + ** is without further significance beyond that single usage. Such a situation often + ** arises in the context of _policy based design_ and flexible configuration of + ** some generic facility, especially when part of this configuration involves + ** the definition of _callback functions._ + ** + ** A common requirement that lends itself well to policy based design arises when such callbacks + ** establish a deeply interwoven link into local implementation details; this kind of interconnected + ** implementation can be expressed succinctly through Lambda functions with contextual capture, and + ** allows to setup a tight interconnection between implementation back-ends, without exposing any + ** of these implementation details at the interface / dependency-injection level. However, the task + ** to package several such specific callbacks into a policy instance and pass this configured policy + ** for creation of service instances, can be surprisingly complex, verbose, confusing and leads + ** to code that is hard to maintain. The reasons for this challenge are manifold: + ** - contextual capture works only well from directly within an implementation function + ** - such a setup can not be expressed well through types and interfaces, since the + ** generated closure is _essentially a value_ and its type can not be spelled out. + ** - a policy with several callbacks thus turns into a template with several type parameters, + ** and must be initialised through a likewise template based constructor; the resulting code + ** tends to become very technical, verbose, surprisingly tricky and hard to decipher for + ** any reader not familiar with all the intricate details of the implementation. + ** + ** The root of these difficulties of proper formulation seems to be related to the nature + ** of cohesive, closely interwoven implementation code. As long as this kind of structure + ** can be kept close together, it works surprisingly well and expresses intent clearly. + ** Yet as soon as it must be taken apart, expressed in names, types and interfaces, + ** a high conceptual burden is added, which leads to the danger of generalisation + ** and re-use beyond the very specific case, to which the original formulation + ** of the problem was well adapted and adequate. + ** + ** The solution promoted here thus aims to remove the need to define any type and interface + ** explicitly; a builder-expression is created rather, which allows to bind all callbacks + ** and further parameters directly inline, within a single constructor invocation. + ** Such a setup becomes possibly through a combination of some C++20 features, linked + ** together by lambda expressions generated from a _macro_. + ** - the PropBuilder acts as base class for the generated records, and is used to + ** initiate the build-up of the definition expression. + ** - several »layers« of derived classes are added step by step, where each such + ** layer adds one further _property_ attribute, that is initialised by a given value. + ** - the _junctor_ to combine those layer is provided by the template function + ** `PropBuilder::define(builder-λ, value)`. It uses the new _explicit object_ + ** syntax introduced with C++20, to pick up the "self type" as template argument. + ** - by invoking this builder function, the current "this"-object is _moved_ into + ** the builder, where it is used to create and initialise a derived class. + ** - for each »layer« a specific lambda is generated by a macro — and it is this + ** technique that allows to accept a lexical token form the program text and + ** use this token as field name in the next derived class to generate. + ** @see PropBuilder_test for detailed explanation + ** @see OutputProxyProvider_test for a usage example */ @@ -35,13 +87,19 @@ namespace meta{ /** * Anchor record and builder mechanics for generating data records. + * - start a builder expression by constructing an anonymous `PropBulder()` + * - invoke PropBuilder::define() for each further property to add + * - pass a builder-λ as first argument, and the data value as second + * - generate a suitable builder-λ with the macro `PROP_FIELD(fieldname)` + * - overall, the builder expression generates a RValue, that will usually + * be passed directly to the constructor of another class. * @see PropBuilder_test::simpleUsage */ struct PropBuilder { /** * Core builder operation: invoke a builder-λ to extend »this« - * @param layerBilder a specially crafted invokable, that takes _this object_ + * @param layerBuilder a specially crafted invokable, that takes _this object_ * and the given property value and creates an instance of a derived class. * @note the macro \ref PROP_FIELD below will generate a suitable argument. * @param val an arbitrary (movable) data element to be stored as property. diff --git a/src/steam/engine/buffhandle.hpp b/src/steam/engine/buffhandle.hpp index 5dddec896..784941408 100644 --- a/src/steam/engine/buffhandle.hpp +++ b/src/steam/engine/buffhandle.hpp @@ -96,7 +96,7 @@ namespace engine { operator HashVal() const { return subClassification_; } - /** convenience shortcut to start a buffer handling cycle */ + /** callback to engage buffer handling cycle */ uint announce (uint count); BuffHandle lockBuffer(); }; diff --git a/tests/core/steam/engine/output-proxy-provider-test.cpp b/tests/core/steam/engine/output-proxy-provider-test.cpp index 857d02049..19046a6c2 100644 --- a/tests/core/steam/engine/output-proxy-provider-test.cpp +++ b/tests/core/steam/engine/output-proxy-provider-test.cpp @@ -21,6 +21,7 @@ //#include "steam/play/diagnostic-output-slot.hpp" #include "steam/engine/buffer-proxy-adaptor.hpp" #include "steam/engine/test-rand-ontology.hpp" +#include "lib/meta/prop-builder.hpp" #include "lib/test/diagnostic-output.hpp" @@ -28,6 +29,7 @@ namespace steam { namespace engine{ namespace test { + using lib::meta::PropBuilder; @@ -42,38 +44,44 @@ namespace test { virtual void run (Arg) { - struct Callback - { - void on_lock() { cout << "LOCK" << endl; } - void on_emit() { cout << "EMIT" << endl; } - void on_release() { cout << "RELEASE" << endl; } - }; + BufferState state{NIL}; // setup with notification callback - BufferProxyAdaptor proxPro{Callback()}; + BufferProxyAdaptor proxPro{PropBuilder() + .define (PROP_FIELD(on_lock), [&]{ state = LOCKED; }) + .define (PROP_FIELD(on_emit), [&]{ state = EMITTED;}) + .define (PROP_FIELD(on_release),[&]{ state = FREE; }) + }; // Assuming some data block is »given« seedRand(); size_t frameNr = defaultGen.u64(); TestFrame dataBlock (frameNr); - CHECK ( dataBlock.isPristine()); + CHECK (dataBlock.isPristine()); + CHECK (state == NIL); BuffHandle handle = proxPro.lockBuffer (dataBlock); + CHECK (state == LOCKED); // Now a »client« can do awful things to the buffer... CHECK (handle.isValid()); auto& data = handle.accessAs(); uint64_t param = defaultGen.u64(); ont::manipulateFrame (&data, &data, param); + HashVal check = data.getChecksum(); // »client« is done... handle.emit(); + CHECK (state == EMITTED); // end usage cycle handle.release(); + CHECK (state == FREE); CHECK (not handle.isValid()); CHECK (not dataBlock.isPristine()); CHECK ( dataBlock.isValid()); + + CHECK (check == dataBlock.getChecksum()); } }; diff --git a/tests/library/meta/prop-builder-test.cpp b/tests/library/meta/prop-builder-test.cpp index ae548605d..9cc202698 100644 --- a/tests/library/meta/prop-builder-test.cpp +++ b/tests/library/meta/prop-builder-test.cpp @@ -21,38 +21,20 @@ #include "lib/test/run.hpp" #include "lib/test/test-helper.hpp" #include "lib/meta/prop-builder.hpp" -//#include "lib/meta/tuple-helper.hpp" -//#include "meta/typelist-diagnostics.hpp" -//#include "meta/tuple-diagnostics.hpp" -//#include "lib/format-string.hpp" -//#include "lib/format-cout.hpp" -//#include "lib/hetero-data.hpp" -#include "lib/test/diagnostic-output.hpp"////////////////TODO +#include "lib/format-string.hpp" #include using std::string; -using lib::test::showType; -//using lib::test::showSizeof; -//using util::toString; -//using util::_Fmt; -//using std::is_same_v; -//using std::make_tuple; -//using std::get; +using util::_Fmt; namespace lib { namespace meta { namespace test { - - - namespace { // test data - - - } // (End) test data - + using lib::test::showType; #define TYPE(_TY_) showType() - + /*********************************************************************//** @@ -73,6 +55,9 @@ namespace test { /** @test show syntax and usage + * - create from the PropBuilder + * - add named fields with values as desired + * - the result is a record holding the data values */ void simpleUsage() @@ -115,7 +100,8 @@ namespace test { /** @test demonstrate and verify the language features * used to generate arbitrary record data types. * - capture self-type with C++20 _explicit object member function_ - * - use generic-λ to generate a derived class with a property field + * - use generic-λ to generate a derived class with a property field + * - chain-up further layers with named fields by extending self-type */ void explain_building_blocks() @@ -139,12 +125,12 @@ namespace test { // local type can be _referred to_ only locally, within this function, // it is _not a hidden type_. Data elements using such a local type // can be returned by-value — which allows us to "drop off" such - // a data record, and this also implicitly its type. + // a data record, and thus also implicitly export its type. // While this seems rather pointless in itself, it can be leveraged // with the help of C++20 explicit lambda template parameters: we can // move an instance of a base class into a function, thereby pick up - // its type, and define a new derived class, and even move the given + // its type, then define a new derived class, and even move the given // base class instance into the embedded base class sub-object: auto dropperLambda = [](BAS&& bas, VAL&& val) @@ -176,53 +162,106 @@ namespace test { ushort evil = 55; auto compoundRecord = dropperLambda (SomeRecord{0.13}, evil); - // here SomeRecord was created transiently, moved into the dropper-λ, // used there as base for a derived struct DataLayer_something, which // features an additional property `something`, initialised with an - // arbitrary value (and type) also passed on invocation. + // arbitrary value (and type), also passed directly on invocation. // The result is a record object, which incorporates the base, together // with an additional "Layer" holding the new property, stored efficiently. + CHECK (compoundRecord.something == evil); + CHECK (compoundRecord.someData == 0.13f); using Compound = decltype(compoundRecord); -SHOW_TYPE(decltype(compoundRecord)) -SHOW_EXPR(TYPE(compoundRecord)) -SHOW_EXPR(sizeof(SomeRecord)) CHECK (sizeof(SomeRecord) == sizeof(float)); -SHOW_EXPR(sizeof(Compound)) CHECK (sizeof(Compound) >= sizeof(float)+sizeof(ushort)); -SHOW_EXPR(alignof(SomeRecord)) -SHOW_EXPR(alignof(Compound)) CHECK (alignof(Compound) == alignof(SomeRecord)); CHECK (sizeof(Compound) == 2 * alignof(float)); -SHOW_EXPR(std::is_class_v) -SHOW_EXPR(std::is_trivial_v) -SHOW_EXPR(std::is_trivial_v) -SHOW_EXPR(std::is_trivially_copyable_v) -SHOW_EXPR(std::is_trivially_copyable_v) -SHOW_EXPR(std::is_standard_layout_v) CHECK (std::is_class_v); CHECK (std::is_trivial_v); CHECK (std::is_trivial_v); CHECK (std::is_trivially_copyable_v); CHECK (std::is_trivially_copyable_v); - // Yet obviously it is not a POD, + // Yet obviously the generated record is not a POD, // since non-static members reside at several levels CHECK (not std::is_standard_layout_v); -SHOW_EXPR((is_Subclass_v)) CHECK ((is_Subclass_v)); + + + // Lastly, a chain of such »property layers« can be built gradually, + // with the help of a hook-method, that grabs the _current record_ + // and passes it to the next extension-layer-λ. As first example, + // the dropper-λ defined above can be used here too... + auto record1 = PropBuilder() + .define (dropperLambda, 0.23) + ; + using Rec1 = decltype(record1); + CHECK ((is_Subclass_v)); + CHECK (sizeof(Rec1) == sizeof(double)); + CHECK (TYPE(record1.something) == "double"_expect); + CHECK (record1.something == 0.23); + + // Note that the name of the property field, here `something`, + // needs to be hard wired into the builder-λ, due to the nature of C++. + // Yet this hurdle can be overcome by generating the builder-λ from a *macro* + auto record2 = record1 + .define (PROP_FIELD(anything) + ,[&](auto val){ return val + evil; } // ◁———————————— can pass a Lambda as "value" + ); + using Rec2 = decltype(record2); + CHECK ((is_Subclass_v)); + CHECK ((is_Subclass_v)); + CHECK (record2.something == 0.23); + CHECK (record2.anything(11) == 66); + evil = 13; + CHECK (record2.anything(42) == 55); + // note the anything-λ is _generic_ + CHECK (record2.anything(7.77f) == 20.77f); + + // Warning: record1 has been moved-away into record2, + // so its content is now »undefined« } - /** @test show how to use generated records to implement a policy. + /** Example for demonstration of possible use-case */ + template + class Producer + : POL + { + public: + Producer (POL policy) + : POL{move(policy)} + { } + + decltype(auto) + doIt() + { + return POL::wrap (POL::feed()); + } + }; + + /** @test show a didactical example how generated records + * can be used to support policy-based design. + * - the Producer template defined above can be configured + * with appropriate actions and operations that fit together + * - the test features a special context-bound producer. */ void demonstrate_usage_scenario() { + uint current{21}; + _Fmt putIt{"%s tralala"}; + + auto doer = Producer{PropBuilder() + .define (PROP_FIELD(feed), [&]{ return current++; }) + .define (PROP_FIELD(wrap), [&](auto it)->string { return putIt % it; }) + }; + + CHECK (doer.doIt() == "21 tralala"_expect); + CHECK (22 == current); } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 431147082..94d3576dd 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -58600,9 +58600,9 @@ - + - + @@ -58620,7 +58620,8 @@ - + + @@ -58633,7 +58634,8 @@ - + + @@ -58644,20 +58646,48 @@ - + + - + + - - - - - + + + + + + + + + + + + + + +

+ Nehme davon Abstand. +

+

+ Da ich als Beispiel eine Struktur wie in einer Factory zeigen möchte, erweist es sich als ausgesprochen schwierig, die erwartete Struktur sauber als Concept zu repräsentieren. Das liegt darin, daß eine solche generische Factory oft implizit auf eine ∀-Quantisierung hinausläuft; d.h. man möchte etwas bauen, das »für beliebige Typen funktioniert«. Wenngleich auch klar ist, daß man niemals völlgig allgemeingültigen, generischen Code schreiben kann, baut man die Struktur doch dieser Idee entsprechend auf. So etwas läßt sich jedoch nicht als einfacher, formal prüfbarerer Constraint formulieren, denn das würde eine formalisierte Spezifikations-Sprache und einen Theorem-Beweis erfordern. +

+

+ +

+

+ Die Concepts in C++ können jedoch nur für einen konkret gegebenen, einzelnen Typ jeweils prüfen, ob sich ein valider Ausdruck ergibt; »valide« würde sich in diesem Fall auf reine Syntax beschränken, und das rechtfertigt nicht den syntaktischen Overhead für ein Concept +

+ +
+ + + +
-
@@ -114207,6 +114237,36 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension)
+ + + + + + + +

+ ...denn dort kommt es zu der Anforderung, bei den State-Transitions jeweils einen Callback aufzurufen; das wirft die Frage nach den Namen und den jeweils übergebenen Daten auf; die Daten wären im Grunde genommen jeweils gleich. +

+ +
+
+ + + + +

+ Das ist eine Konsequenz aus der Einsicht, daß der metadata::Entry nicht nur der internen Verwaltung dient, sondern die dort aufgezeichneten Informationen auch essentiell sind für die Implementierung eines Storage-Backends. Das ist ein weithin bekanntes Pattern (und Problem) für Allokatoren: man muß die Storage-Size kennen, speziell auch zur De-Allokation. Wenn man diese Einsicht verbindet mit Überlegungen zum globalen Speicherblock-Management in der Engine, dann wird schnell klar, daß man bisweilen durchaus einen zu-großen-Speicherblock verwenden möchte; das bedeutet dann, daß die Size-Eintrag für den state-Key entsprechend präzise ausgefüllt sein muß. +

+ +
+
+ + + + + + + @@ -114348,8 +114408,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -114549,17 +114609,17 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + - + - + @@ -114572,11 +114632,11 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + - + @@ -114627,15 +114687,25 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + - - - + + + - + + + + + + + + + + + @@ -114909,6 +114979,43 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + + + + + + +

+ leichte Uneinheitlichkeit bzgl. der jeweils erreichten States, und auch die implizite Perspektive betreffend (wer sagt wem was); »release« könnte auch »free« heißen +

+ +
+ +
+ + + + +

+ effektiv sind es jeweils die gleichen Infos für jeden Transition, aber sie sind nicht eindeutig angeordnet +

+ +
+
+ + +
+
@@ -114974,6 +115081,10 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + +