HiveBrain v1.2.0
Get Started
← Back to all entries
patterncppMinor

Optionally Lazy Parameters

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
parameterslazyoptionally

Problem

Inspired by Swift's @autoclosure feature, I tried writing a brief C++-14 header that permits "optionally" lazy parameters (by "lazy" I mean @autoclosure-like; I chose the word "lazy" in emulation of lazy evaluation in languages such as Haskell). From this post on interesting Swift features:


The @autoclosure attribute delays the execution of a function that's activated in a function parameter. Essentially, calling a function inside of a parameter will wrap the function call in a closure for later use in the function body.

I provide a macro, LAZY_PARAM, that can be applied to parameters in function declarations; when calling these functions, arguments must be supplied using either the LAZY_ARG macro, which uses reference-based closure-semantics to delay evaluation of the expression, or the EAGER_ARG macro, which immediately evaluates the expression and wraps it in a light-weight class that simply stores the result. It's copied below and also available here. (NOTE: The linked version, on GitHub, will be kept up-to-date as I make changes, but the copied version may not.)

``
#pragma once

#include
#include

// Uses dynamic dispatch to permit lazy OR eager argument evaluation at the
// caller's discretion, with uniform access to the final value from the callee's
// perspective.
template
class LazyType_Base
{
protected:
// For LazyType_TrueLazy
template
LazyType_Base(CALLABLE&& expr)
{
static_assert(
std::is_same::type, VAL_TYPE
>::value,
"Expression does not evaluate to correct type!");
}
// For LazyType_Eager
// =default is not permitted by GCC here; see
// https://stackoverflow.com/q/38213809/1858225
LazyType_Base(void) {}

public:
// Both compilers suddenly fail when this is introduced, attempting to
// instantiate
LazyType_Base>`, which makes no
// sense. See https://stackoverflow.com/q/38214138/1858225
// In general, not making the destructor virtual

Solution

Does this seem like a reasonable idea for a lightweight library? Is there anything similar that's already available? I like the idea of it for functions that may or may not require actually using particular arguments, e.g. in a logger with a "sensitivity level"

Well, passing parameters lazily sounds like a good idea; but I don't really see why the callee needs to be aware of both "lazy" and "non-lazy" params. In other words, I would reimplement your whole library as three lines:

#define OPTIONALLY_LAZY(T) std::function
#define LAZY_ARG(e) [&](){ return e; }
#define EAGER_ARG(e) [_x=(e)](){ return _x; }


and then force the callee to be implemented as

void PermitLazy(
    OPTIONALLY_LAZY(int) my_int)
{
  std::cout << "Called 'PermitLazy'." << std::endl;
  std::cout << "Got possibly-lazy int: " << my_int() << std::endl;
}


(notice the one extra set of parentheses in my_int() there).

Furthermore, I would offer the caller the option to lazily compute the value on first reference and then cache that value for all future references:

#define LAZY_MEMOIZED_ARG(e) \
    [&, _first=true, _x=decltype(e){}]() mutable { \
        if (_first) { _x = (e); _first = false; } \
        return e; \
    }


You certainly can reinvent-the-wheel of std::function while you're at it, but you don't need to reinvent it. The standard one will probably be faster than your thing.

Implicit conversions (e.g. your operator T()) are the devil and should be avoided. For example, consider the semantics of std::max(my_int, 0) with your library (and compare to the semantics of std::max(my_int(), 0) with my three-liner). Also consider what happens if you pass your my_int to a function expecting a const int&. (Off the top of your head, what does it do? What should it do? Now try it — what does it really do?)

Raw rvalue references (e.g. your LazyType_Base&&) are also a code smell to be avoided; in these cases, if you can't take by const lvalue reference, you usually ought to be taking by value.

Your &->auto {...} is just a very long-winded way of writing [&]{...}. Personally I do write [&](){...} with the "unnecessary" parentheses, and wouldn't dock you for those; but writing out (void) in C++, or writing ->auto at all, is definitely unidiomatic.

Code Snippets

#define OPTIONALLY_LAZY(T) std::function<T()>
#define LAZY_ARG(e) [&](){ return e; }
#define EAGER_ARG(e) [_x=(e)](){ return _x; }
void PermitLazy(
    OPTIONALLY_LAZY(int) my_int)
{
  std::cout << "Called 'PermitLazy'." << std::endl;
  std::cout << "Got possibly-lazy int: " << my_int() << std::endl;
}
#define LAZY_MEMOIZED_ARG(e) \
    [&, _first=true, _x=decltype(e){}]() mutable { \
        if (_first) { _x = (e); _first = false; } \
        return e; \
    }

Context

StackExchange Code Review Q#133991, answer score: 2

Revisions (0)

No revisions yet.