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 <type_traits>
#include <utility>
// 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 <typename VAL_TYPE>
class LazyType_Base
{
protected:
// For LazyType_TrueLazy
template <typename CALLABLE>
LazyType_Base(CALLABLE&& expr)
{
static_assert(
std::is_same<
typename std::remove_reference<decltype(expr())>::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<LazyType_Base<VAL_TYPE>>`, which makes no
// sense. See https://stackoverflow.com/q/38214138/1858225
// In general, not making the destructor virtual *should* be safe as long as
// users are just using this type as it's intended (i.e. for lazy arguments
// to functions, to be used immediately or discarded without passing
// pointers around for deletion later).
// virtual ~LazyType_Base(void) =default;
virtual operator VAL_TYPE(void) =0;
};
// Takes an arbitrary expression and creates a class that will evaluate the
// expression when necessary.
template <typename VAL_TYPE, typename CALLABLE>
class LazyType_TrueLazy : public LazyType_Base<VAL_TYPE>
{
// friend LazyType_TrueLazy<VAL_TYPE, CALLABLE> MakeLazy<CALLABLE>(CALLABLE&& expr);
// With a working friend declaration, make all constructors private; `protect`
// default constructors in `LazyType_Base`
public:
LazyType_TrueLazy(CALLABLE&& expr)
: LazyType_Base<VAL_TYPE>{std::forward<CALLABLE>(expr)}
, expr_{std::forward<CALLABLE>(expr)}
{}
operator VAL_TYPE(void) override final
{
return expr_();
}
private:
CALLABLE expr_;
};
// If VAL_TYPE is the same as EXPR_TYPE, then we do not have a truly lazy
// instantiation.
template <typename VAL_TYPE>
class LazyType_Eager : public LazyType_Base<VAL_TYPE>
{
public:
LazyType_Eager(
VAL_TYPE&& final_val)
: LazyType_Base<VAL_TYPE>{}
, val_{final_val}
{}
operator VAL_TYPE(void) override final
{
return val_;
}
private:
VAL_TYPE val_;
};
// C++14: since template `auto` arguments are not permitted prior to C++17,
// we need a helper function to evaluate the type of the callable, in case it's
// a lambda.
template <typename CALLABLE>
auto MakeLazy(CALLABLE&& expr)
{
return LazyType_TrueLazy<
typename std::remove_reference<decltype(expr())>::type, CALLABLE>{
std::forward<CALLABLE>(expr)};
}
#define LAZY_ARG(expr) \
MakeLazy([&](void)->auto { return expr; })
// Is there some way to create an implicit conversion to `LazyType_Eager` for
// all types that will be applied for any attempted conversion to
// `LazyType_Base`?
#define EAGER_ARG(val) \
LazyType_Eager<typename std::remove_reference<decltype(val)>::type>{val}
// Must provide a REFERENCE so that the actual type will not be sliced...but we
// don't actually want a mutable reference to a long-lived object, so we take an
// r-value reference. This is very simple to use with the `LAZY_ARG` and
// `EAGER_ARG` macros.
#define OPTIONALLY_LAZY(type) \
LazyType_Base<type>&&
I've also created a short program to test it:
#include <iostream>
#include "OptionallyLazy.hpp"
void PermitLazy(
OPTIONALLY_LAZY(int) my_int)
{
std::cout << "Called 'PermitLazy'." << std::endl;
std::cout << "Got possibly-lazy int: " << my_int << std::endl;
}
int ExpensivelyGenerateInt(void)
{
std::cerr << " <[( Generating int (pretend this call is expensive)! )]> ";
std::cerr.flush();
return 7;
}
int main(void)
{
PermitLazy(LAZY_ARG(ExpensivelyGenerateInt()));
PermitLazy(EAGER_ARG(ExpensivelyGenerateInt()));
PermitLazy(EAGER_ARG(3));
// See comment above `EAGER_ARG`
// PermitLazy(ExpensivelyGenerateInt());
}
In the above "test" code, STDERR
is interleaved with STDOUT
in a way that demonstrates that indeed the EAGER
expression is evaluated immediately, while the LAZY
expression is evaluated after the Called 'PermitLazy'
print-statement.
The above code compiles with G++ 5.1 and Clang++ 3.7.
I'd like to know:
- 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": when the sensitivity level is below the severity level, there's no reason to actually construct the log-entry string, and in some cases this might actually be rather expensive (e.g. when dumping an entire JSON tree for debugging purposes).
- Ideally, I'd like to be able to implicitly convert any type to a
LazyType_Base<T>&&
by automatically creating aLazyType_Eager
without requiring theEAGER_ARG
macro. I think this would make usage a little simpler and improve code readability at the call-site. Is there any way to accomplish this? (I don't think it's possible to create a constructor in a base class that creates an instance of a particular derived class under-the-hood, because...that makes no sense. And I can't think of another way to accomplish this.) - Are there any hidden pitfalls that people can see? Any other improvements I can make?
EDIT: links to SO questions:
- G++ doesn't permit use of
=default
constructor for base-class template - Existence of virtual destructor changes evaluation of seemingly-unrelated expression's type SOLVED
EDIT: other known issues:
const
-correctness: This apparently does not work "out-of-the-box" forconst
types. I'm not sure why, but the compiler errors seem to indicate that this creates a temporary object of typeLazyType_Base
, which can't be done because it's pure-virtual. I don't know exactly why using aconst
value-type causes the creation of the temporary, though, when the non-const
version does not.
1 Answer 1
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<T()>
#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<T(void)>
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<type>&&
) 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 [&](void)->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.
-
\$\begingroup\$ I'm not sure I really understand your very first comment: "I don't really see why the callee needs to be aware of both "lazy" and "non-lazy" params." You then show an implementation that forces the callee to use
()
to get the actual value, but I don't see what this has to do with "understanding" lazy and non-lazy params. \$\endgroup\$Kyle Strand– Kyle Strand2016年07月06日 16:06:31 +00:00Commented Jul 6, 2016 at 16:06 -
\$\begingroup\$ Additionally, your guess that
std::function
is faster is actually not correct; I've added some trivial benchmarking to the Git repository and run it a couple times on my machine. You can look at the git repo to see exactly what I did, but with 10,000,000 calls to an "optionally lazy" function,std::function
took about 1.14 seconds for lazy args and 0.68 seconds for eager args, while my implementation took about 0.26 seconds and 0.18 seconds, respectively. \$\endgroup\$Kyle Strand– Kyle Strand2016年07月06日 19:17:49 +00:00Commented Jul 6, 2016 at 19:17 -
\$\begingroup\$ This makes sense with what I've heard about the general performance of pure lambdas versus
std::function
. \$\endgroup\$Kyle Strand– Kyle Strand2016年07月06日 19:18:16 +00:00Commented Jul 6, 2016 at 19:18 -
1\$\begingroup\$ I feel that forcing the callee to explicitly retrieve the argument value using
()
defeats the entire point of "lazy arguments", but then again attempting to implement lazy evaluation in a low-level eagerly-evaluating language like C++ is definitely running against the grain, and indeed I don't see a way around the fundamental problem that this attempt at deferred evaluation via wrapping types wreaks havoc with template functions likemax
. \$\endgroup\$Kyle Strand– Kyle Strand2016年07月06日 19:26:50 +00:00Commented Jul 6, 2016 at 19:26 -
\$\begingroup\$ As for the insistence that a particular language feature is "the devil," that feels...contrary to the goals of C++, which seems to be designed to provide anything and everything the committee can think of and leave it up to developers to decide how to use the tools provided. This is why I thought it might be possible to implement this "optionally lazy" thing in the first place. \$\endgroup\$Kyle Strand– Kyle Strand2016年07月06日 19:33:37 +00:00Commented Jul 6, 2016 at 19:33
@autoclosure
andLazy Parameters
. This is not something I have heard of before. \$\endgroup\$@autoclosure
definition. "Lazy parameters" is not a technical term AFAIK, but simply my chosen term for what I'm doing here; I'm drawing on the idea of eager versus lazy evaluation (e.g. when comparing languages like Haskell, which do lazy evaluation, to "eager" languages like C++). \$\endgroup\$