17

Is it possible to access (read only) the variables captured by a lambda?

This doesn't work:

std::function<double  (const double)> plus (const double a) {
    return [a] (const double b) -> double {
        return a+b;
    };
}

auto plus5 = plus(5);
cout << plus5.a << endl;
Uyghur Lives Matter
  • 18,820
  • 42
  • 108
  • 144
user357269
  • 1,835
  • 14
  • 40

3 Answers3

16
auto plus( double a ) {
  using R = struct {
    double a;
    double operator()(double b)const{return b+a;}
  };
  return R{std::move(a)};
}

live example.

Please note that a std::function is not a lambda, and lambda is not a std::function. They work with each other, but using one term to refer to the other is the opposite of helpful.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • Why a `using` definition instead of simply naming the struct? – StoryTeller - Unslander Monica Aug 29 '16 at 13:24
  • 1
    @StoryTeller I am trying to mimic a lambda as close as possible, basically. My original attempt was to directly return the anonymous struct, but declaring a struct in a return statement is not allowed. ;) – Yakk - Adam Nevraumont Aug 29 '16 at 13:35
  • Thanks Yakk, this is great! Is it possible to make the struct inherit from some C++ function interface so it can be specialised/converted an `std::function` when I don't need access to `a` anymore? – user357269 Aug 29 '16 at 13:35
  • 3
    @user357269 No need. `std::function` just works. Magic. Poof. Wave your hands and watch it happen. Read [type erasure](https://stackoverflow.com/documentation/c%2b%2b/2872/type-erasure#t=20160829133822308369) if you want to know what is going on under the hood. – Yakk - Adam Nevraumont Aug 29 '16 at 13:37
  • Unfortunately this code leads to unnecessary copying of `a` twice! – user357269 Sep 01 '16 at 12:47
  • @user357269 First of all, it is a double. 8 bytes is very cheap to copy, and as a primitive type the compiler can as-if eliminate many copies easily. Second, elision should eliminate the return value copies. There is one copy into plus (which matches your code), and one copy into the pseudo-lambda (which could be changed to a move), and every other copy should be elided (ie, not exist as a distinct object, but be merged with the lifetime of another object, with no bits moving around). – Yakk - Adam Nevraumont Sep 01 '16 at 13:26
  • @Yakk: sorry, my comment wasn't very clear. I'm not concerned about copying doubles. I would like to use your snippet for real applications with types where a move is substantially cheaper than a copy. I ran the code and only actually found one copying of `a`: when the initialization list constructor of R is called. That copy can be avoided by using @ComicSansMS's more verbose answer. – user357269 Sep 01 '16 at 13:56
  • 1
    @user357269 Or by inserting a `std::move` there, as I just did. :) Instrument with a `struct noisy` that prints when copied (but not when moved). If you move it into `plus` and store the return value, a total of zero copies will be made. [live example of noisy](http://coliru.stacked-crooked.com/a/cf0339efb840dedc) – Yakk - Adam Nevraumont Sep 01 '16 at 14:42
  • @Yakk-AdamNevraumont: Even `using` names the struct “for linkage purposes”. You could introduce a *typedef-name* for, say, a pointer to it… – Davis Herring Sep 21 '19 at 00:58
14

This is not how a lambda should be used.

The interface of a lambda is its function signature. Its captures should be considered an implementation detail and not be visible to the user.

If you want explicit access to the captures, write your own function object and expose the respective data members accordingly:

struct MyPlus {
    double a;
    MyPlus(double x) : a(x) {}
    double operator()(const double b)
    {
        return a+b;
    }
};

auto plus5 = MyPlus(5);
std::cout << plus5.a;
ComicSansMS
  • 51,484
  • 14
  • 155
  • 166
  • 1
    This is exactly the sort of boilerplate that I use to lambdas to avoid – user357269 Aug 29 '16 at 12:43
  • 5
    @user357269 Yes. But this is a very specific situation which lambdas are simply not fit to handle. So I am afraid that falling back to the more verbose, but more powerful generic approach is your best option. – ComicSansMS Aug 29 '16 at 12:44
12

"Definitely not after storing it in a std function. Without that, I could do it with a horrible (yet legal) hack in C++17. But I would be a horrible person to tell you how, because you might use it." – Yakk

Well let's relieve Yakk's karma; here's a proof of concept of a C++14 solution which you definitely don't want to let loose in the wild:

auto magic = [a, b](auto &&... args) mutable -> decltype(auto) {
    return makeOverload(

        // Capture access boilerplate
        [&](cap_<0>) -> auto& { return a; },
        [&](cap_<1>) -> auto& { return b; },

        // Actual function
        [&](int p) {
            return "[" + std::to_string(a) + ", " + b + "](" + std::to_string(p) + ")";
        }

    )(std::forward<decltype(args)>(args)...);
};

makeOverload takes any number of functors and blends them into a single one. I borrowed the idea from this blog post, with help from the comment section to make it actually work.

The resulting functor is used to tag-dispatch between the cap<N> tags and the actual parameters of the function. Thus, calling magic(cap<0>) causes it to spit out the corresponding captured variable, that is a. The actual behaviour of the function is, of course, still accessible with a normal call to magic(123).

As a bonus, the outer lambda is mutable, and the capture accessors return by reference: you actually have read-write access to the captured variables!

You can observe and interact with this creature in its natural habitat on Coliru right here.

Community
  • 1
  • 1
Quentin
  • 62,093
  • 7
  • 131
  • 191