1

How can I accurately predict from a capture which type of member will be created in the lambda?

In C++, I thought that capturing an object of type T by value creates a data member of type const T, and by reference T&. But when compiling this:

#include <iostream>

struct A{
    A(){std::cout<<"A\n";}
    A(const A&){std::cout<<"A&\n";}
    void cf()const{}
    void f(){}
};


int main(){
    A a;
    A& ra=a;
    const A& cra=a;
    auto f00 = [ra, cra, &a]()-> void{
        //Fixed:
        //ra is A, cra is const A, a is A&
        //lambda is void operator()()const
        a.cf(); a.f();//pass
        //ra.cf(); ra.f();//ra.f compilation err. 
        //cra.cf(); cra.f();//cra.f compilation err
    };
    //f00(); //A&,A&

    auto f01 = [ra, cra, &a]()mutable-> void{
        //Fixed:
        //ra is A, cra is const A, a is A&
        //lambda is void operator()()mutalbe
        a.cf(); a.f();//pass
        ra.cf(); ra.f();//pass
        cra.cf(); cra.f();//cra.cf pass, but cra.f error, why?
    };
    //f01(); //A&,A&

    auto f02 = [&ra, &cra, &a]()mutable-> void{
        //Fixed:
        //ra is A&, cra is const A&, a is A&
        //lambda is void operator()()mutable
        a.cf(); a.f();//pass
        ra.cf(); ra.f();//pass
        //cra.cf(); cra.f();//cra.cf pass, but cra.f error, why?
    };
    f02(); //
    return 0;
}

I encountered the following compilation error:

test_lambda.cpp:26:25: error: passing 'const A' as 'this' argument discards qualifiers [-fpermissive]
         cra.cf(); cra.f();//pass, cra.f error
                         ^
test_lambda.cpp:8:10: note:   in call to 'void A::f()'
     void f(){}
          ^

Does this mean that cra has really been captured by reference, rather than by a copy of the referred object as I expected?

Yunxi Ye
  • 11
  • 2

1 Answers1

1

The type of the captured entities remains the same, except that references to objects are captured as copies of the referenced objects. From CPP Reference on Lambda Closure Types:

The type of each data member is the type of the corresponding captured entity, except if the entity has reference type (in that case, references to functions are captured as lvalue references to the referenced functions, and references to objects are captured as copies of the referenced objects).

In all of your lambdas, the type of the closure member cra is A. They are not, themselves, const. However, the default function-call operator() of the lambda is. The error on line 17 about f00 is caused by the fact that you attempt to modify an closure member created by copy when calling ra.f(), but due to it having an operator() const, it can only perform const operations on its members.

This is why in all three functions calling the non-const A::f on cra gives a compilation error. You should add mutable after the lambda argument list to allow performing non-const operations on by-copy closure members.

underscore_d
  • 6,309
  • 3
  • 38
  • 64
paul-g
  • 3,797
  • 2
  • 21
  • 36
  • That's obvious. But it doesn't explain why "//ra.f compilation err. ". `ra` - is not const, `f` is not const also. – Arkady Aug 15 '16 at 10:54
  • 1
    cra is captured as `const A`, not `const A&`. – Jarod42 Aug 15 '16 at 12:01
  • @Jarod42 Mhm, are you sure? Check my update – paul-g Aug 15 '16 at 12:19
  • @paul-g By wrapping the argument to `decltype` in `(parentheses)`, you artifically make it into an expression having type of the object but with a reference added. You shouldn't do that. We want the real type of the object. So just ask for that: `decltype(cra)`, etc. See: http://stackoverflow.com/a/3097803/2757035 – underscore_d Aug 15 '16 at 12:39
  • Please post a link to a Coliru or etc live demo of this code where the captures become reference types. This does not make sense to me, and I cannot seem to replicate it, although I've had to start with simpler examples due to type_name seemingly not being supported by Coliru (or me missing something). – underscore_d Aug 15 '16 at 12:54
  • @underscore_d good catch. Updated the output. type(a) makes sense, type(cra) is still `const A&` – paul-g Aug 15 '16 at 12:54
  • @underscore_d see the demo http://coliru.stacked-crooked.com/a/a486b7366a5d8dd6 – paul-g Aug 15 '16 at 12:58
  • 1
    From your quote: *"references to objects are captured as copies of the referenced objects"* – Jarod42 Aug 15 '16 at 12:59
  • I have no idea what that mess is doing or what `type_name` is and whether it even knows to use the lambda member scope rather than the enclosing function, but anyway! Regardless of how you achieved it, this output goes completely against what the Standard specifies and what I've always experienced with lambdas. Have a look at `int main(){int a{42}; int &b{a}; auto f = [b]() { b = 64; }; f(); return 0; }` => `clang++ -std=c++14 main.cpp` => `main.cpp:7:7: error: cannot assign to a variable captured by copy in a non-mutable lambda` with the key words being _captured by copy_ – underscore_d Aug 15 '16 at 13:06
  • `g++` => `main.cpp:7:9: error: assignment of read-only variable 'b' b = 64;` - proving again that the capture takes a copy of the referred object, unless told explicitly to take a reference. Why do I say "proving"? because if we write `int i{42}; int const &c{i}; c = 4;` we get instead `error: assignment of read-only reference 'c'` See the difference? – underscore_d Aug 15 '16 at 13:09
  • That's better. :P Nitpick time: A "constant lambda" to most would mean `auto const thingy = [] { doStuff(); }`. Rather, what OP declared was a non-`const` lambda, but it defaults to having an internal `operator() const`, so it cannot modify (non-`const`) members. Adding `mutable` means that becomes just plain old `operator()` and hence can modify non-`const` member variables from the capture. And, needless to say, it makes no sense to declare an `auto const []` with a `() mutable` call operator, since if the lambda itself is `const`, then even its nominally non-`const` members must be `const`. – underscore_d Aug 15 '16 at 13:26
  • @underscore_d, @Jarod42 thx for your comments. I agree, but even a simple test with: `auto f00 = [ra, cra, a]() -> void{ std::cout << std::is_reference() << " " << std::is_reference << " " << std::is_reference() << std::endl; }; f00();` shows that cra and ra are reference types, while a is not reference type. Any other way we can verify this? ` – paul-g Aug 15 '16 at 13:28
  • @paul-g I suspect this might somehow by due to scope lookup/shadowing. If you capture the reference by-copy with a different name - which shouldn't change anything except the name - and then do `is_reference` on _that_, you'll get a `0`. Now I'm really confused... Assuming `int i{42}; int &r{i}; auto f = [r] { /* ... */ };`, then maybe the call to `typeid` is resolved and inlined in the lambda before `f.r` ever exists - and so always refers to `main()::r` for that reason? :S I think you might want to start a separate thread for this one! – underscore_d Aug 15 '16 at 13:31
  • @underscore_d nice, looks like you're right, updating to `auto f00 = [rra = ra, cra, a]() ...` ==> `is_reference == 0` – paul-g Aug 15 '16 at 13:36
  • @paul-g Either way, e.g. adding some diagnostic `std::cout << &r << std::endl;` will confirm that the lambda has a copy of the original object that resides at a different address. So _that_ part of the lambda definitely knows which `r` we're trying to use, i.e. the one in the closest scope, a copy of the value to which the 'captured' reference referred. – underscore_d Aug 15 '16 at 13:36
  • @paul-g **A-ha**: http://stackoverflow.com/a/33854742/2757035 `Every id-expression […] of an entity captured by copy is transformed into an access to the [closure member]. [Note: An id-expression that is not an odr-use refers to the original entity, never to a member of the closure` `decltype()` is not an _odr-use_, so it always prefers the original name, rather than the captured copy of the same name. Capturing with a different name circumvents this, so `decltype()` must return the type of the capture. So, tangential to the OP but very interesting questions, leading us to an important caveat! – underscore_d Aug 15 '16 at 13:40
  • Here is a Coliru example showing exactly what happens in this scenario, proving that capturing a reference by copy does in fact copy its referred object, and how _odr-use_ affects all this and let `decltype` cause all of our confusion: http://coliru.stacked-crooked.com/a/c08f2ac37eebd42f – underscore_d Aug 15 '16 at 14:41
  • simpler example would be to delete copy constructor... – Jarod42 Aug 15 '16 at 14:50
  • @paul-g I've suggested an edit. Most importantly, as we discussed but wasn't clarified in the answer - the captured types are not `const`, only the default `operator()` is. I also did a fair bit of rearranging/slight elaboration for clarity, which I hope you find is useful. I can't really post my own answer because it would still be largely based on yours, so that'd be a bit plagiaristic. :D – underscore_d Aug 15 '16 at 15:02