3

I have various functions in my codebase that take a generic callable object and pass it to a series of nested lambdas before calling it. Example:

template <typename TF>
void interface(TF&& f)
{
    nested0([/*...*/]()
        {
            nested1([/*...*/](auto& x)
                {
                    nested2([&x, /*...*/]()
                        {
                            f(x);
                        });
                });
        });
}

Note that interface is taking a callable object of type TF by forwarding reference (previously known as universal reference). The callable object is usually a lambda with various captured variables, both by value and by reference.

What is the best (in terms of performance) way of capturing f in the nested lambdas while maintaining correctness?

I can think of three options:

  1. Capture f by copy.

    nested0([f]()
    {
        nested1([f](auto& x)
            {
                nested2([&x, f]()
                    {
                        f(x);
                    });
            });
    });
    

    Could cause unnecessary copies, and if the captured object is mutable it could cause incorrect behavior.

  2. Capture f by reference.

    nested0([&f]()
    {
        nested1([&f](auto& x)
            {
                nested2([&x, &f]()
                    {
                        f(x);
                    });
            });
    });
    

    Seems reasonable, but could cause problems if any of the nested lambdas perform an action that outlives the owner of f. Imagine if nested2's body was executed in a separate thread - f could already be out of scope.

  3. Make the lambdas mutable and capture by perfect-forwarding.

    #define FWD(...) std::forward<decltype(__VA_ARGS__)>(__VA_ARGS__)
    
    nested0([f = FWD(f)]() mutable
    {
        nested1([f = FWD(f)](auto& x) mutable
            {
                nested2([&x, f = FWD(f)]() mutable
                    {
                        f(x);
                    });
            });
    });
    

    The lambdas have to be mutable because we're potentially moving f from a lambda to another one. This approach seems to avoid unnecessary copies and to correctly move the callable object if it needs to outlive the original caller.

Is option 3 always the best one, or does it have any potential drawback? ...or maybe there is no "best and correct" way at all (knowledge about the callable object is required)?

Vittorio Romeo
  • 90,666
  • 33
  • 258
  • 416
  • why do you use forward in the inner lambdas, when `f` does no longer refer to the function parameter? – Piotr Skotnicki Apr 22 '16 at 11:17
  • @PiotrSkotnicki: because I still have to capture the outer lambda's `f` from the "current" lambda. If `interface`'s `f` was moved into the first lambda, I'd like to move the first lambda's `f` into the second one as well, and so on. – Vittorio Romeo Apr 22 '16 at 11:24
  • what I mean is that this is equivalent to move in the inner lambdas – Piotr Skotnicki Apr 22 '16 at 11:25
  • @PiotrSkotnicki: isn't that only true if it was moved from `interface` to the first lambda? I think that if `f` was an lvalue-reference then it would never be moved and just references in every nested lambda. – Vittorio Romeo Apr 22 '16 at 11:35
  • it's captured by value at each level – Piotr Skotnicki Apr 22 '16 at 11:37
  • @PiotrSkotnicki: ah, [you're correct](http://coliru.stacked-crooked.com/a/03479993ff770c5f). I wrongfully assumed that it would get perfectly forwarded at every level, but there's no "forwarding reference" to do that. Do you suggest forwarding only in the first layer then capturing by reference? – Vittorio Romeo Apr 22 '16 at 11:55
  • this depends on what you do with the inner lambdas, when they are called etc. this question lacks those details, presumably the reason for no answer – Piotr Skotnicki Apr 22 '16 at 15:57
  • Actually it's quite difficult to give you an answer with such a poor context. Can you provide a minimal, working example on which to reason? – skypjack Apr 22 '16 at 20:23
  • @PiotrSkotnicki, skypjack - the goal of the question was finding a potentially "best" way that worked with any callable. "There is no 'best' way and the most efficient/correct code depends on the passed callable" is an acceptable answer. – Vittorio Romeo Apr 23 '16 at 09:11

1 Answers1

1

As mentioned in the comments, it's difficult to say with such a poor context given about the problem.
That said, the best solution seems to me to capture everything by reference and break this logic whenever you need a copy the aim of which is to outlive the lifetime of f, as an example:

nested0([&f]() {
    n1([&f](auto& x) {
        n2([&x,
                // get a local copy of f
                f{std::forward<TF>(f)}]() {
            f(x);
        });
    });
});

Anyway, there is no rule of thumb for such a problem.
The best solution is tightly bound likely to the actual problem.
As usual. Fair enough.

skypjack
  • 49,335
  • 19
  • 95
  • 187