-
Type: Task
-
Resolution: Unresolved
-
Priority: Major - P3
-
None
-
Affects Version/s: None
-
Component/s: None
-
Query Optimization
-
QO 2024-04-01, QO 2024-04-15, QO 2024-04-29, QO 2024-05-13, QO 2024-05-27, QO 2024-06-10, QO 2024-06-24, QO 2024-07-08, QO 2024-07-22, QO 2024-08-05, QO 2024-08-19, QO 2024-09-02, QO 2024-09-16, QO 2024-09-30, QO 2024-10-14
TL;DR:
A quick win for improved usability of the internal Deferred helper, reducing memory usage and runtime overhead. No externally visible impact (unless Deferred is used in a particularly hot location, in which case a mild perf benefit may be seen in benchmarks).
The Deferred template is used to lazily compute a value.
The template params will often be deduced:
auto foo = Deferred([](){...});
However, CTAD does not apply to member variables.
struct Foo { Deferred<???> member = [](){}; };
Lambdas cannot be conveniently named in the above case as the parameter type.
Non-capturing lambdas can, however, decay to function pointers.
struct Foo { Deferred<int(*)()> member = [](){return 1;}; };
Users do not always enjoy function pointer types. For convenience, we should add DeferredFnPtr (through analogy to the existing DeferredFn, wrapping an std::function).
struct Foo { DeferredFn<int, std::string> member = [&](std::string foo){return someCapture;}; DeferredFnPtr<int, std::string> member = [](std::string foo){return 1;}; };
However, both of these pay additional cost to store an std::function or function ptr respectively, and may inhibit some optimisations.
See godbolt for size cost of std::function.
Returning to the case of:
auto foo = Deferred([](){...});
As the lambda has no capture, it will be zero sized. By using [[no_unique_address]] (or other common solutions), there will be no space overhead for storing an initializer function in this case. It would be ideal for this to be possible in a member variable, without inconveniences such as:
struct Foo { static constexpr auto _foo = [](){return 1;}; Deferred<decltype(_foo)> member = foo; };
One potential solution is the use of C++20 support for stateless lambdas in unevaluated contexts; that is, extend Deferred to support:
struct Foo {
DeferredParam<[]() {return 1;}> member;
};
Another option is to assess current usages and expand upon the abstraction.
For example, this is a common pattern:
struct Foo { DeferredFn<int> member = [](){return someGlobalAtomic.loadRelaxed();}; };
As noted, this pays an unnecessary cost by using an std::function for a compile-time known method, which is not rebound during the life of the DeferredFn in most cases. Recognising this, a more specific type could be added:
struct Foo { CachedGlobal<someGlobalValue> member; };
This could be trivially implemented using DeferredParam.
This cleanly expresses the author's intent - this member will always read from, then cache this specific value. The interface could be tailored to common usages; e.g., specialised for Atomic to add matching methods to control the memory ordering used when loading the global value.