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.
@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\$