Search code examples
c++lambda

Lambda loses captured value


I cannot understand why the captured value is lost. I understand that it is connected with an out of scope or copying of an object of LambdaWrapper. But what exactly happens? If LambdaWrapper(100) leaves the scope in Add and reference to __value loses, then why there is no same to LambdaWrapper(300).

#include <iostream>
#include <vector>
#include <functional>
using namespace std;

class LambdaWrapper {
public:
    LambdaWrapper(double new_value): __value (new_value) {
        cout << "constructed with " << __value << endl;
        __func = [this](){ cout << __value << endl;};
    }
    void call() const { __func(); }
private:
    double __value;
    function<void(void)> __func;
};

class LambdaContainer {
public:
    void Add(double value) {
        LambdaWrapper w(value); //out of scope
        parts.push_back(w);
    }

    void Add(LambdaWrapper w) // passed as value
    {
        parts.push_back(w);
    }

    void call() const {
        for (const auto& part : parts)
                part.call();
    }
private:
    vector<LambdaWrapper> parts;
};

int main() {
    LambdaContainer c;
    c.Add(100);

    LambdaWrapper w(200);
    c.Add(w);

    c.Add( LambdaWrapper(300) ); //LambdaWrapper(300) will out of scope too

    cout << "==============" << endl;
    c.call();

    return 0;
}

The output:

constructed with 100
constructed with 200
constructed with 300
==============
6.95168e-308 <<< WHY?
200
300

Solution

  • I suppose this question is asking what happens, rather than "Is it ok", so in this case (usually) gdb is your friend, modifying the program to print the address of this during construction, this inside __func and the actual address of the object in the container we see: (addresses varies, but distances and concepts should remain the same)

    # During constructors and captured value:
    0x7fffffffda80 <- LambdaWrapper(100)
    0x7fffffffdb00 <- LambdaWrapper(200)
    0x7fffffffdb60 <- LambdaWrapper(300)
    # Actual address of data stored in the container:
    0x6170c0 <- LambdaWrapper(100)
    0x6170e8 <- LambdaWrapper(200)
    0x617110 <- LambdaWrapper(300)
    

    There is a huge value difference, this is because the creation happens on the stack, while the vector allocates data with new on the heap.

    From gdb calling info proc mappings we obtain the memory addresses list:

    Mapped address spaces:
    
              Start Addr           End Addr       Size     Offset objfile
                0x400000           0x404000     0x4000        0x0 /[...]/LambdaOutOfScope/a.out
                0x603000           0x604000     0x1000     0x3000 /[...]/LambdaOutOfScope/a.out
                0x604000           0x605000     0x1000     0x4000 /[...]/LambdaOutOfScope/a.out
                0x605000           0x637000    0x32000        0x0 [heap]
    
    [...]
    
          0x7ffffffde000     0x7ffffffff000    0x21000        0x0 [stack]
      0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]
    

    But this does not answer why only the 100 is changed. The answer to the problem resides in the stack frames: each function call has a local (usually small) variable space for static variables (those without new for simplicity).

    If we check the stack info with info frame we see that:

    (gdb) info frame
    Stack level 0, frame at 0x7fffffffdbb0:
     rip = 0x400deb in main (main.cpp:75); saved rip = 0x7ffff7495830
     source language c++.
     Arglist at 0x7fffffffdba0, args: 
     Locals at **0x7fffffffdba0**, Previous frame's sp is 0x7fffffffdbb0
     Saved registers:
      rbx at 0x7fffffffdb98, rbp at 0x7fffffffdba0, rip at 0x7fffffffdba8
    

    Inside main, so the 100 stays out of this frame, because it is not constructed in main but in Add, to check, when inside Add we get:

    (gdb) info frame 1
    Stack frame at 0x1:
     rip = 0x0; saved rip = <not saved>
     Outermost frame: previous frame identical to this frame (corrupt stack?)
     Arglist at 0x7fffffffdac8, args: 
     Locals at **0x7fffffffdac8**, Previous frame's sp is 0x7fffffffdad8
     Saved registers:
      rip at 0x7fffffffdad0
    

    So the corruption happens when we call another function, but since the elements allocated in main are local they are preserved, if you put a c.Add(400); after the 300 you'll see it corrupted too (even if constructed after).

    NOTE: I hope to have covered all, but gdb usage in detail, there are plenty of guides on the internet.