3
\$\begingroup\$

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 a LazyType_Eager without requiring the EAGER_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:

EDIT: other known issues:

  • const-correctness: This apparently does not work "out-of-the-box" for const types. I'm not sure why, but the compiler errors seem to indicate that this creates a temporary object of type LazyType_Base, which can't be done because it's pure-virtual. I don't know exactly why using a const value-type causes the creation of the temporary, though, when the non-const version does not.
\$\endgroup\$
7
  • \$\begingroup\$ Can you add a link to for @autoclosure and Lazy Parameters. This is not something I have heard of before. \$\endgroup\$ Jul 5, 2016 at 22:18
  • \$\begingroup\$ @LokiAstari Thanks for the suggestion; I've provided a link and quoted the @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\$ Jul 5, 2016 at 22:27
  • \$\begingroup\$ The version on this question can't be updated as you make changes, aside from changes to make the question on-topic. \$\endgroup\$
    – anon
    Jul 6, 2016 at 2:33
  • \$\begingroup\$ @QPaysTaxes Is that a rule specific to CodeReview? This is (as you can see) my first question, and I'm not familiar with that rule. \$\endgroup\$ Jul 6, 2016 at 3:04
  • \$\begingroup\$ @KyleStrand It is. Sorry, I didn't realize you were new here -- welcome! In broad strokes, questions have to be about real, working code (which yours is, assuming you didn't put a placeholder or broken library there) and once the code is up, it can't be modified, unless the question is off-topic for some reason and you're fixing it. I'd recommend reading through some help center articles, and poking through the most popular questions and answers on Meta. They explain better than I can. \$\endgroup\$
    – anon
    Jul 6, 2016 at 3:08

1 Answer 1

2
\$\begingroup\$

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.

\$\endgroup\$
14
  • \$\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\$ 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\$ 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\$ 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 like max. \$\endgroup\$ 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\$ Jul 6, 2016 at 19:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.