diff --git a/src/lib/lazy-init.hpp b/src/lib/lazy-init.hpp index 3fdbc5592..4d3107a48 100644 --- a/src/lib/lazy-init.hpp +++ b/src/lib/lazy-init.hpp @@ -54,7 +54,7 @@ ** that the »trojan functor« itself is stored somehow embedded into the target object ** to be initialised. If there is a fixed distance relation in memory, then the target ** can be derived from the self-position of the functor; if this assumption is broken - ** however, memory corruption and SEGFAULT may be caused. + ** however, memory corruption and SEGFAULT may be caused. ** ** @todo 11/2023 at the moment I am just desperately trying to get a bye-product of my ** main effort into usable shape and salvage an design idea that sounded clever @@ -73,15 +73,12 @@ #define LIB_LAZY_INIT_H -//#include "lib/error.h" -//#include "lib/nocopy.hpp" +#include "lib/error.h" #include "lib/meta/function.hpp" #include "lib/opaque-holder.hpp" -//#include "lib/meta/function-closure.hpp" -//#include "lib/util-quant.hpp" #include "lib/util.hpp" -//#include +#include #include #include @@ -90,21 +87,23 @@ namespace lib { namespace err = lumiera::error; using lib::meta::_Fun; + using lib::meta::_FunArg; using lib::meta::has_Sig; -// using std::function; using util::unConst; + using std::function; using std::forward; using std::move; using RawAddr = void const*; + namespace {// the anonymous namespace of horrors... inline ptrdiff_t captureRawAddrOffset (RawAddr anchor, RawAddr subject) { // Dear Mr.Compiler, please get out of my way. - // I just sincerely want to shoot myself into my foot... + // I just genuinely want to shoot myself into my foot... char* anchorAddr = reinterpret_cast (unConst(anchor)); char* subjectAddr = reinterpret_cast (unConst(subject)); return subjectAddr - anchorAddr; @@ -140,7 +139,10 @@ namespace lib { "apply small-object optimisation with inline storage."}; return captureRawAddrOffset (functor,payload); }(); - } + // + }//(End)low-level manipulations + + /** @@ -171,12 +173,11 @@ namespace lib { * and then to forward the invocation to the actual * function, which should have been initialised * by the delegate invoked. - * @param delegate a functor object pass invocation; + * @param delegate a functor object to forward invocation; * the delegate must return a reference to the * actual function implementation to invoke. * Must be heap-allocated. * @return a lightweight lambda usable as trigger. - * @note takes ownership of the delegate */ template static auto @@ -195,10 +196,18 @@ namespace lib { - /** - * + + + struct EmptyBase { }; + + /**************************************************************//** + * Mix-in for lazy/delayed initialisation of an embedded functor. + * This allows to keep the overall object (initially) copyable, + * while later preventing copy once the functor was »engaged«. + * Initially, only a »trap« is installed into the functor, + * invoking an initialisation closure on first use. */ - template + template class LazyInit : public PAR { @@ -209,8 +218,10 @@ namespace lib { using HeapStorage = InPlaceBuffer; using PendingInit = std::shared_ptr; + /** manage heap storage for a pending initialisation closure */ PendingInit pendingInit_; + PendingInit const& __trapLocked (PendingInit const& init) { @@ -232,13 +243,27 @@ namespace lib { } + + protected: + struct MarkDisabled{}; + + /** @internal allows derived classes to leave the initialiser deliberately disabled */ + template + LazyInit (MarkDisabled, ARGS&& ...parentCtorArgs) + : PAR(forward (parentCtorArgs)...) + , pendingInit_{} + { } + + public: + /** prepare an initialiser to be activated on first use */ template LazyInit (std::function& targetFunctor, INI&& initialiser, ARGS&& ...parentCtorArgs) : PAR(forward (parentCtorArgs)...) , pendingInit_{prepareInitialiser (targetFunctor, forward (initialiser))} { } + LazyInit (LazyInit const& ref) : PAR{ref} , pendingInit_{__trapLocked (ref.pendingInit_)} @@ -272,6 +297,12 @@ namespace lib { } + bool + isInit() const + { + return not pendingInit_; + } + template void installEmptyInitialiser() @@ -279,7 +310,15 @@ namespace lib { pendingInit_.reset (new HeapStorage{emptyInitialiser()}); } - private: + template + void + installInitialiser (std::function& targetFunctor, INI&& initialiser) + { + pendingInit_ = prepareInitialiser (targetFunctor, forward (initialiser)); + } + + + private: /* ========== setup of the initialisation mechanism ========== */ template DelegateType emptyInitialiser() @@ -306,7 +345,7 @@ namespace lib { template DelegateType* - getPointerToDelegate(HeapStorage& buffer) + getPointerToDelegate (HeapStorage& buffer) { return reinterpret_cast*> (&buffer); } @@ -316,6 +355,7 @@ namespace lib { buildInitialiserDelegate (std::function& targetFunctor, INI&& initialiser) { using TargetFun = std::function; + using ExpectedArg = _FunArg; return DelegateType{ [performInit = forward (initialiser) ,targetOffset = captureRawAddrOffset (this, &targetFunctor)] @@ -324,9 +364,10 @@ namespace lib { TargetFun* target = relocate (location, -FUNCTOR_PAYLOAD_OFFSET); LazyInit* self = relocate (target, -targetOffset); REQUIRE (self); - performInit (self); - self->pendingInit_.reset(); - return *target; + // invoke init, possibly downcast to derived *self + performInit (static_cast (self)); + self->pendingInit_.reset(); // release storage + return *target; // invoked by the »Trojan« to yield first result }}; } }; diff --git a/tests/library/lazy-init-test.cpp b/tests/library/lazy-init-test.cpp index 002bf2d8b..da13eb8f6 100644 --- a/tests/library/lazy-init-test.cpp +++ b/tests/library/lazy-init-test.cpp @@ -34,7 +34,7 @@ #include "lib/test/diagnostic-output.hpp" /////////////////////TODO TODOH #include "lib/util.hpp" -//#include +#include @@ -42,10 +42,10 @@ namespace lib { namespace test{ // using util::_Fmt; + using std::make_unique; using util::isSameObject; using lib::meta::isFunMember; -// using lib::meta::_FunRet; -// using err::LUMIERA_ERROR_LIFECYCLE; + using err::LUMIERA_ERROR_LIFECYCLE; @@ -78,6 +78,7 @@ namespace test{ verify_TargetRelocation(); verify_triggerMechanism(); verify_lazyInitialisation(); + verify_complexUsageWithCopy(); } @@ -87,7 +88,7 @@ namespace test{ * # the _target function_ finally to be invoked performs a verifiable computation * # the _delegate_ receives an memory location and returns a reference to the target * # the generated _»trojan λ«_ captures its own address, invokes the delegate, - * retrieves a reference to a target functor, and invokes these with actual arguments. + * retrieves a reference to a target functor, and finally invokes this with actual arguments. * @remark the purpose of this convoluted scheme is for the _delegate to perform initialisation,_ * taking into account the current memory location „sniffed“ by the trojan. */ @@ -119,16 +120,15 @@ namespace test{ return fun; }; using Delegate = decltype(delegate); - Delegate *delP = new Delegate(delegate); + auto delP = make_unique (delegate); // verify the heap-allocated copy of the delegate behaves as expected location = nullptr; CHECK (beacon+c == (*delP)(this)(c)); CHECK (location == this); - // now (finally) build the »trap function«, - // taking ownership of the heap-allocated delegate copy - auto trojanLambda = TrojanFun::generateTrap (delP); + // now (finally) build the »trap function«... + auto trojanLambda = TrojanFun::generateTrap (delP.get()); CHECK (sizeof(trojanLambda) == sizeof(size_t)); // on invocation... @@ -138,7 +138,7 @@ namespace test{ CHECK (beacon+c == trojanLambda(c)); CHECK (location == &trojanLambda); - // repeat that with a copy, and changed beacon value + // repeat same with a copy, and changed beacon value auto trojanClone = trojanLambda; beacon = rand(); c = beacon % 55; @@ -150,17 +150,17 @@ namespace test{ - /** @test verify that std::function indeed stores a simple functor inline + /** @test verify that std::function indeed stores a simple functor inline. * @remark The implementation of LazyInit relies crucially on a known optimisation * in the standard library ─ which unfortunately is not guaranteed by the standard: * Typically, std::function will apply _small object optimisation_ to place a very * small functor directly into the wrapper, if the payload has a trivial copy-ctor. - * Libstdc++ is known to be rather restrictive, other implementations trade increased - * storage size of std::function against more optimisation possibilities. + * `Libstdc++` is known to be rather restrictive, while other implementations trade + * increased storage size of std::function against more optimisation possibilities. * LazyInit exploits this optimisation to „spy“ about the current object location, - * to allow executing the lazy initialisation on first use, without further help + * allowing to execute the lazy initialisation on first use, without further help * by client code. This trickery seems to be the only way, since λ-capture by reference - * is broken after copying or moving the host object (which is required for DSL use). + * is broken after copying or moving the host object (typically required for DSL use). * In case this turns out to be fragile, LazyInit should become a "LateInit" and needs * help by the client or the user to trigger initialisation; alternatively the DSL * could be split off into a separate builder object distinct from RandomDraw. @@ -169,7 +169,7 @@ namespace test{ verify_inlineStorage() { // char payload[24];// ◁─────────────────────────────── use this to make the test fail.... - const char* payload = "Outer Space"; + const char* payload = "please look elsewhere"; auto lambda = [payload]{ return RawAddr(&payload); }; RawAddr location = lambda(); @@ -196,7 +196,7 @@ namespace test{ * by applying known offsets consecutively * from a starting point within an remote instance * @remark in the real usage scenario, we know _only_ the offset - * and and attempt to find home without knowing the layout. + * and attempt to find home without knowing the layout. */ void verify_TargetRelocation() @@ -229,7 +229,7 @@ namespace test{ CHECK (offNested > 0); // create a copy far far away... - auto farAway = std::make_unique (here); + auto farAway = make_unique (here); // reconstruct base address from starting point RawAddr startPoint = farAway->peek(); @@ -322,6 +322,89 @@ namespace test{ CHECK (1 == invoked); CHECK (init); } + + + + /** elaborate setup used for integration test */ + struct LazyDemo + : LazyInit<> + { + using Fun = std::function; + + int seed{0}; + Fun fun; // ◁────────────────────────────────── this will be initialised lazily.... + + template + auto + buildInit (FUN&& fun2install) + { + return [theFun = forward (fun2install)] + (LazyDemo* self) + { + CHECK (self); + self->fun = [self, chain = move(theFun)] + (int i) + { + return chain (i + self->seed); // Note: binding to actual instance location + }; + }; + } + + + LazyDemo() + : LazyInit{MarkDisabled()} + , fun{} + { + installInitialiser(fun, buildInit([](int){ return 0; })); + } + + template + LazyDemo(FUN&& someFun) + : LazyInit{MarkDisabled()} + , fun{} + { + installInitialiser(fun, buildInit (forward (someFun))); + } + }; + + /** + * @test use an elaborately constructed example to cover more corner cases + * - the function to manage and initialise lazily is _a member_ of the _derived class_ + * - the initialisation routine _adapts_ this function and links it with the current + * object location; thus, invoking this function on a copy would crash / corrupt memory. + * - however, as long as initialisation has not been triggered, LazyDemo instances can be + * copied; they may even be assigned to existing instances, overwriting their state. + */ + void + verify_complexUsageWithCopy() + { + LazyDemo d1; + CHECK (not d1.isInit()); // not initialised, since function was not invoked yet + CHECK (d1.fun); // the functor is not empty anymore, since the »trap« was installed + + d1.seed = 2; + CHECK (0 == d1.fun(22)); // d1 was default initialised and thus got the "return 0" function + CHECK (d1.isInit()); // first invocation also triggered the init-routine + + // is »engaged« after init and rejects move / copy + VERIFY_ERROR (LIFECYCLE, LazyDemo dx{move(d1)} ); + + + d1 = LazyDemo{[](int i) // assign a fresh copy (discarding any state in d1) + { + return i + 1; // using a "return i+1" function + }}; + CHECK (not d1.isInit()); + CHECK (d1.seed == 0); // assignment indeed erased any existing settings (seed≔2) + CHECK (d1.fun); + + CHECK (23 == d1.fun(22)); // new function was tied in (while also referring to self->seed) + CHECK (d1.isInit()); + d1.seed = 3; // set the seed + CHECK (26 == d1.fun(22)); // seed value is picked up dynamically + + VERIFY_ERROR (LIFECYCLE, LazyDemo dx{move(d1)} ); + } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 8e62e33f3..0fff39895 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -97280,6 +97280,93 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ die Funktion ist dieses Mal ein Feld im abgeleiteten Objekt (yess!) +

+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -97451,12 +97538,97 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ hier besteht ein latentes semantsiches Problem: +

+

+ lazyInit ⟹ Objekt ist erst mal noch nicht ganz initialisiert +

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

+ dieses Design ist MIST +

+ + +
+
+
+
+
+