Jonathan Boccara's blog

A Generic Component for Out-of-line Lambdas

Published June 12, 2020 - 0 Comments

When exploring out-of-line lambdas, we saw how we could make a call site using a lambda more expressive by hiding the lambda in a separate function.

We transformed this code that shows low-level details:

auto const product = getProduct();

std::vector<Box> goodBoxes;
std::copy_if(boxes.begin(), boxes.end(), std::back_inserter(goodBoxes),
    [product](Box const& box)
    {
        // low-level details
        const double volume = box.getVolume();
        const double weight = volume * product.getDensity();
        const double sidesSurface = box.getSidesSurface();
        const double pressure = weight / sidesSurface;
        const double maxPressure = box.getMaterial().getMaxPressure();
        return pressure <= maxPressure;
    });

Into this one that replaces the details with a call to a sub-function:

auto const product = getProduct();

std::vector<Box> goodBoxes;
std::copy_if(boxes.begin(), boxes.end(), std::back_inserter(goodBoxes), resists(product));

And we saw that for resists to handle both lvalues and rvalues we resorted to several overloads:

bool resists(Box const& box, Product const& product)
{
    const double volume = box.getVolume();
    const double weight = volume * product.getDensity();
    const double sidesSurface = box.getSidesSurface();
    const double pressure = weight / sidesSurface;
    const double maxPressure = box.getMaterial().getMaxPressure();
    return pressure <= maxPressure;
}

auto resists(Product const& product)
{
    return [&product](const Box& box)
    {
        return resists(box, product);
    };
}

auto resists(Product&& product)
{
    return [product = std::move(product)](const Box& box)
    {
        return resists(box, product);
    };
}

If you’d like to see more details about why this technique makes code more expressive and how this all works, check out the post on out-of-line lambdas.

It would be nice not to have to write the last two overloads, because they’re here for technical reasons only, to handle the life cycle of lvalues and rvalues. We will now design a generic component that will encapsulate this technical layer and generate it for us.

Disclaimer: the following shows indeed a generic component that avoids writing the technical overloads, but I don’t claim it has the optimal design. I will try to outline its limitations. If you see how to improve it, or how to design the component differently, I’ll be pleased to read your feedback in the comments section.

A generic component

What would we like the interface to look like, to begin with?

Ideally, we would not like it to change from what it is now: resists(product) for an lvalue and resists(getProduct()) for an rvalue. After all, what we’re making is related to implementation details, to avoid writing technical code.

If we keep that interface, then resists can’t be a simple function. It needs to be something that contains two functions: one for lvalue products and one for rvalue ones.

How do we put several functions inside of one components? By using a good old function object. Functors are not dead!

Differentiating lvalues and rvalues

Let’s call our function object OutOfLineLambda. We need it to able able to handle both lvalues and rvalues contexts, so it needs two operator()s:

class OutOfLineLambda
{
public:
    template<typename Context>
    auto operator()(Context& context) const
    {
        // we'll implement this in a moment
    }
    
    template<typename Context>
    auto operator()(Context&& context) const
    {
        // this too
    }
};

Those two overloads are not ambiguous: lvalues go to the first one and rvalues go to the second one. Note that both overloads could accommodate both lvalues and rvalues if they were alone. But we need then both in order to differentiate between lvalues and rvalues and have a specific capture behaviour for each case.

Connecting the function object to the implementation

A natural way to pass the body of our business function (resists) to our technical component OutOfLineLambda is to pass it to its constructor and let the function object store it. In order to accommodate various types of callable objects (functions of various prototypes, lambdas, std::functions), we need the function object to be a template:

template<typename Function>
class OutOfLineLambda
{
public:
    explicit OutOfLineLambda(Function function) : function_(function){}
    
    template<typename Context>
    auto operator()(Context& context) const
    {
        // we'll implement this in a moment 
    }
    
    template<typename Context>
    auto operator()(Context&& context) const
    {
        // this too
    }
    
private:
    Function function_;
};

To use our component, we could instantiate it like this:

auto const resists = OutOfLineLambda([](Product const& product, Box const& box)
{
    const double volume = box.getVolume();
    const double weight = volume * product.getDensity();
    const double sidesSurface = box.getSidesSurface();
    const double pressure = weight / sidesSurface;
    const double maxPressure = box.getMaterial().getMaxPressure();
    return pressure <= maxPressure;
});

This definition would be outside of the function containing its usage, just like for a sub-function’s definition.

One limitation of this design is that it is not obvious which parameter is the context. We need to agree on the convention that it is the first one, so that we can have an arbitrary number of parameters for the underlying “real” function.

Indeed, here we use the lambda with std::copy_if whose predicate take only one argument (here, the Box). But other algorithms, such as std::transform, can require function objects that take two arguments.

In that case, we would need our function to look like this:

auto const resists = OutOfLineLambda([](Product const& product, Box const& box, OtherStuff const& anotherThing)
{
    // ...
});

This is why can agree that the context is the first parameter.

Note that this code relies on C++17 template type deduction for constructor arguments. Before C++17, we need to resort to a helper function:

template<typename Function>
OutOfLineLambda<Function> makeOutOfLineLambda(Function function)
{
    return OutOfLineLambda<Function>(function);
}

We would use it this way:

auto const resists = makeOutOfLineLambda([](Product const& product, Box const& box)
{
    const double volume = box.getVolume();
    const double weight = volume * product.getDensity();
    const double sidesSurface = box.getSidesSurface();
    const double pressure = weight / sidesSurface;
    const double maxPressure = box.getMaterial().getMaxPressure();
    return pressure <= maxPressure;
});

Implementing the operator()s

All that’s left is to implement the two operator()s of the function object. Let’s start with the one that takes lvalues. It takes a context (in our case that would be the product), and returns a lambda that takes an arbitrary number of parameters (in our case, one Box) and forwards them to the function (the one inside resists):

template<typename Context>
auto operator()(Context& context) const
{
    return [&context, this](auto&&... objects)
    {
        return function_(context, std::forward<decltype(objects)>(objects)...);
    };
}

The context is captured by reference.

Note that this code relies on the fact that C++14 allows auto parameters in lambdas. Also, we capture this in order to have access to the data member function_.

Finally, the implementation of the operator() is very similar except that it uses a generalized lambda capture in order to move the rvalue reference context into the lambda:

template<typename Context>
auto operator()(Context&& context) const
{
    return [context = std::move(context), this](auto&&... objects)
    {
        return function_(context, std::forward<decltype(objects)>(objects)...);
    };
}

A generic component for out-of-line lambdas

Here is all the code of our generic component put together:

template<typename Function>
class OutOfLineLambda
{
public:
    explicit OutOfLineLambda(Function function) : function_(function){}
    
    template<typename Context>
    auto operator()(Context& context) const
    {
        return [&context, this](auto&&... objects) { return function_(context, std::forward<decltype(objects)>(objects)...); };
    }
    
    template<typename Context>
    auto operator()(Context&& context) const
    {
        return [context = std::move(context), this](auto&&... objects) { return function_(context, std::forward<decltype(objects)>(objects)...); };
    }
    
private:
    Function function_;
};

// Before C++17
template<typename Function>
OutOfLineLambda<Function> makeOutOfLineLambda(Function function)
{
    return OutOfLineLambda<Function>(function);
}

Do you find it makes it easier to define out-of-line lambdas? How would you have designed this component differently? Do you use out-of-line lambdas in your code?

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin