The Shape of Lambda

The Shape of Lambda

16 Jul 2018    

In this one we’ll look at how a lambda is represented and how we can take advantage of it. Given this lambda:

auto const printString = [LineEnd=envLineEnd](std::string const& str) {
    OutputDebugStringA(str.c_str());
    OutputDebugStringA(LineEnd.c_str());
};

Here we have declared a lambda of name printString using the auto keyword. This is a pretty standard declaration but the important bit here is that this wouldn’t work without using auto or templates. This is because lambdas don’t really have a type per se. The type of a lambda is special and is known as the closure type. Each lambda has a unique closure type. And this closure type has a const callable operator (unless you declare the lambda mutable). It is similar to declaring something like this:

struct PrintString {
    // capture by value
    std::string LineEnd;

    PrintString(std::string const& envLineEnd)
    : LineEnd(envLineEnd) {}

    auto operator()(std::string const&) const {}
};

Even lambdas with the same signature, captures and body will have different closure types. In fact, if the compiler will recognize a closure type as a class such that std::is_class<decltype(PrintString)>::value will be true. This also helps the compiler’s inlining optimizations without any special rules.

Often when passing lambdas, you need not use std::function since it incurs a bit of overhead and instead deduce the type using a templated argument. This also provides additional flexibility to use any function object (ex std::less<>). However, std::function has the added advantage of having the signature in it’s declaration making it easier to spot bugs when passing a lambda with the wrong signature. A cheaper alternative would be to use std::function_ref which is part of C++20 or even better would be a Callable C++ concept.

// Same lambda as example above
const auto printString = /*...*/;

template <typename T>
void SetPrintFunc(T printFunc) {}

template <typename T>
class Logger {
public:
    Logger(T logger){};
};

// Given the two examples above we can use either
SetPrintFunc(printString); // No overhead

Logger<decltype(printString)> customLog(printString); // No overhead

Another advantage of using just a template parameter is being able to overload the callable operator on the type to handle different types of function parameters. This is achieved by inheriting the closure type and using it’s call operator.

// Can be made generic in C++ 17
// Since pack expansion is allowed in using expressions
template <typename T, typename U, typename V>
auto MakePrintable(T t, U u, V v) {
    struct Printable : T, U, V {
        Printable(T t, U u, V v) : T(t), U(u), V(v) {}
        
        using T::operator();
        using U::operator();
        using V::operator();
    };

    return Printable{t, u, v};
}

// Given the three different overloads
auto const printString = []( std::string const& str) {};

auto const printSV = [](std::string_view sv) {};

auto const PrintCStr = [](char const* str) {};

// Function needs to be set here
template <typename T>
void SetPrintFunc(T printFunc) {}

// Actual call to setup the print function
SetPrintFunc(MakePrintable(printString, printSV, PrintCStr));
// Thus the corresponding lambda will be used with the apporopriate overload

So the next time around if you’re using a callable type as a parameter you now have several options to choose from. Choose wisely.