2

Reading the answer from Capturing a reference by reference in a C++11 lambda makes me think that the following code generates undefined behavior because of the ended lifetime of i in the lambda-capture. Is that right for C++1y? I am asking because g++ 4.8.2 translates the code just fine.

#include <iostream>

auto captureFct( ) {
    int i=0;
    auto set = [&i](int _i){ i=_i; };
    auto get = [&i](){ return i; };
    return std::pair<decltype(set),decltype(get)>(set,get);
}


int main() {
    auto myPair = captureFct();
    auto set1 = myPair.first;
    auto get1 = myPair.second;

    auto myPair1 = captureFct();
    auto set2 = myPair1.first;
    auto get2 = myPair1.second;

    std::cout << "\nget1:" << get1() << " get2:" << get2() << '\n';
    set1(1); set2(2);
    std::cout << "\nget1:" << get1() << " get2:" << get2();
}

/*
    Local Variables:
    compile-command: "g++ -std=c++1y lambda.cc -o a.exe && ./a.exe"
    End:
 */

The output is interesting:

get1:0 get2:0

get1:2 get2:2

It seems that the same reference is used for all lambdas.

This behavior differs from the behavior of the following elisp code (as close to the c++ code as possible):

(defun captureFct ()
  (lexical-let ((i 0))
    (list :set (lambda (_i) (setq i _i))
      :get (lambda () i))))

(setq myPair (captureFct))
(setq myPair1 (captureFct))

(message "\nget1: %d get2: %d"
     (funcall (plist-get myPair :get))
     (funcall (plist-get myPair1 :get)))

(funcall (plist-get myPair :set) 1)
(funcall (plist-get myPair1 :set) 2)

(message "\nget1: %d get2: %d"
     (funcall (plist-get myPair :get))
     (funcall (plist-get myPair1 :get)))

The output of the elisp code is:

get1: 0 get2: 0

get1: 1 get2: 2

I think I know already the answer. But, I post this question anyway since it is interesting for folks that do both elisp and c++.

Last but not least a C++ version that works like the elisp version:

#include <iostream>
#include <memory>

auto captureFct( ) {
    std::shared_ptr<int> pi(new int(0));
    auto set = [pi](int _i){ *pi=_i; };
    auto get = [pi](){ return *pi; };
    return std::pair<decltype(set),decltype(get)>(set,get);
}


int main() {

    auto myPair = captureFct();
    auto set1 = myPair.first;
    auto get1 = myPair.second;

    auto myPair1 = captureFct();
    auto set2 = myPair1.first;
    auto get2 = myPair1.second;



    std::cout << "\nget1:" << get1() << " get2:" << get2() << '\n';
    set1(1); set2(2);
    std::cout << "\nget1:" << get1() << " get2:" << get2();
}

/*
    Local Variables:
    compile-command: "g++ -std=c++1y lambda.cc -o a.exe && ./a.exe"
    End:
 */
Community
  • 1
  • 1
Tobias
  • 5,038
  • 1
  • 18
  • 39

1 Answers1

1

Yes, it's undefined behaviour, since the references inside the lambdas become dangling as soon as captureFunc() exits (*).

What's probably happening in your case is that the references (which are just pointers under the hood) still point to the space on the stack where i was on the first invocation of captureFunc(), and it ends up in exactly the same location on the second invocation of captureFunc(); so the net effect is that all of get1, get2, set1, set2 have their internal i reference pointed to the same (currently unused) location in memory, so they modify it for each other.

Of course, the above is just speculation, and could change next time you (or run) the program, since Undefined Behaviour is Undefined.

(*) Quoting C++11, [expr.prim.lambda]§22:

[ Note: If an entity is implicitly or explicitly captured by reference, invoking the function call operator of the corresponding lambda-expression after the lifetime of the entity has ended is likely to result in undefined behavior. —end note ]

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
  • Yes, now I discovered the note "If an entity is implicitly or explicitly captured by reference, invoking the function call operator of the corresponding lambda-expression after the lifetime of the entity has ended is likely to result in undefined behavior." in the quite recent document http://isocpp.org/files/papers/N3797.pdf. This is already stated in http://stackoverflow.com/questions/21443023/capturing-a-reference-by-reference-in-a-c11-lambda. Would be nice if you could include that into your answer somehow. Then I will accept it. I think g++ could and should warn about that. – Tobias May 08 '14 at 07:48
  • @Tobias Added the standard quote – Angew is no longer proud of SO May 08 '14 at 09:05
  • 1
    @Tobias: Note that this is just a non-normative note - dangling references created by lambda capture is simply a special case of the more general rule about returning references to locals being undefined behavior, that's why it's only mentioned in passing in the lambda chapter. – JohannesD May 08 '14 at 16:19
  • @JohannesD: IMO the note is quite important since the lisp example shows that it can be done in another way (combined lexical + dynamic binding). Even in C++ there are ways to prolong the livetime of objects. An example is `std::exception_ptr` for exceptions. But, you are right, that the syntax of C++ for lambdas makes it quite clear that there results a dangling reference at this point. – Tobias May 08 '14 at 16:30
  • In my opinion a compiler warning would be appropriate if elements captured by reference in a lambda go out of scope but the lambda survives. – Tobias May 08 '14 at 16:32