Skip to content

Latest commit

 

History

History
99 lines (68 loc) · 4.26 KB

File metadata and controls

99 lines (68 loc) · 4.26 KB

Take benefit from simple laziness

In the C++ world lazy evaluation is usually linked to templates and their property that separates definition from actual instantiation. Given that we can, for instance, delay binding of a symbol:

void register(int*);
void deregister(int*);

template<class Tag>
struct delayed_registrator
{
    delayed_registrator() { register(&id); }
    ~delayed_registrator() { deregister(&id); }
};

where symbol id is not avaiable at the point of definition, but it is avaiable at the point of instantiation:

const int id = 1234;
struct device_tag {};

delayed_registrator<device_tag> registered;

Functions in register and deregister must be there — name lookup is done at the point of definition (but argument-dependent lookup at instantiation, live example), so that at least forward declarations of register and deregister must be available before definition of delayed_registrator. Type is templated over Tag which does the "delay effect" we wanted to achieve.

Besides the above, we have old plain short circuiting inherited from C language: logical operators like && (and), || (or) and ternary operator ?:. They can be used as constructions to lazy execute (expressions must be valid C++) some of the expressions. Note, "not executed" is not the same as "not evaluated" (like decltype, noexcept, sizeof, typeid or requires). With short circuiting we want to delay or skip execution of costly operations.

Ordering with and

Simple ordering by the increasing processing cost can save a lot of runtime resources and decrease execution time:

const bool can_progress
    =    invariant()            // constant cost
      && has_environment()      // medium cost: verifies in-memory structures
      && check_network_nodes(); // huge cost: does blocking IO

This technique recalls Linux kernels likely and unlikely macros used in if-else conditionals, which give possibility to expliticly favour one of the execution branches.

Pattern matching with or

Let's consider deserialisation engine that provides family of functions of prototype:

template<class V>
bool deserialise(const S& from, variant<int, string, C>& into);

that return true if we can deserialise from value into value of type V which is one of the types under variant. User provides value of type S through:

bool deserialize_any(const S& from);

We need to check inside the body of deserialize_any whether we can deserialise into value of one of the supported types, do the deserialisation and return with status. One can think about cascade of if-elseif-else blocks or switch-case construct, but we will use short circuiting to do kind of pattern matching over a set of varianted types:

bool deserialise_any(const S& from, variant<int, string, C>& into)
{
    return 
           deserialise<int>(from, into)     // smallest cost
        || deserialize<string>(from, into)  // bigger cost: must deserialize sequence of chars
        || deserialize<C>(from, into);      // huge cost: custom data type
}

Function deserialise_any will stop execution at first true returned from subsequent deserialise call. This is classic understanding of pattern matching, where all the cases are tested in order.

Construction with ?:

In some cases we cannot construct value with default constructor (and then update its state), then we use ternary operator:

const T t{ exists ? "right here" : reset_value()};
//                  ^~~ cheap      ^~~ costly

where exists of true is more likely than of false. We can put above to the extremum and play with comma operator:

void fill(V&&);

bool fill_valid(V&& v)
{
    return v.valid() ? (fill(v), true) : false;
}

Please note that we do not use negated boolean logic, that is we always check for true not for false in the condition. That's good for readbility.

About this document

June 13, 2016 — Krzysztof Ostrowski

LICENSE