Search code examples
c++lambdatemporary-objects

Use std::function as member function, which capture `this`, and access it from the copied lambda after destructor


Flex Ferrum post a code sample here (I think it is Minimal, Complete, and Verifiable enough):

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

class Bang
{
public:
    Bang(int i = 0) : m_val(i)
    {
        m_foo = [this] {std::cout << m_val << std::endl;};
    }

    ~Bang()
    {
        m_val = -1;
    }

    void Foo()
    {
        m_foo();
    }
private:
    int m_val;
    std::function<void ()> m_foo;
};

Bang GetBang()
{
    return Bang(100500);
}

int main() {
    Bang b(100500);
    b.Foo();
    b = GetBang();
    b.Foo();
    return 0;
}

Our nice Flex also offer a live demo


After a rough look, I thought it will output 100500, but the real output is:

-1
  • Why? What's behind it?
  • How to fix it?(output 100500, not -1)

I have written some my own reasonings in the ask box, but found it is more fit to be posted as an answer(will make the question too long). If my answer is wrong, please correct it and more answers are welcome


Solution

  • Ah, it should blame the destructor of temporary - Bang(100500), which returns form GetBang, is prvalue, and has temporary object lifetime.

    1. [this] will be stored as reference of *this, like this:

        class Lambda
        {
        public:
            void operator()() const
            {
                //output
            }
    
        private:
            Bang& bang;
    
        public:
            Lambda(Bang& bang) : bang{bang}
            {
            }
    
        } lambda{*this};
        ...
        m_foo = lambda;
    

    1. Because here is no RVO, so, the temporary Bang(100500) will first be assigned to b, then be destroyed.

    2. Custorm operator(), constructor, and destructor to output some information:


    #include <iostream>
    #include <functional>
    
    using namespace std;
    
    class Bang
    {
    public:
        Bang(int i = 0) : m_val(i)
        {
    
            std::cout << "Bang(int i = 0) m_val address is " << &m_val << '\n';
            class Lambda
            {
            public:
                void operator()() const
                {
    
                    std::cout << "operator() m_val address is " << &bang.m_val << '\n';
                    std::cout << bang.m_val << std::endl;
                }
    
            private:
                Bang &bang;
    
            public:
                Lambda(Bang &bang) : bang{bang}
                {
                }
    
            } lambda{*this};
            m_foo = lambda;
    
        }
    
        ~Bang()
        {
            std::cout << "~Bang()\n";
            m_val = -1;
        }
    
        void Foo()
        {
            m_foo();
        }
    
    private:
        int m_val;
        std::function<void()> m_foo;
    };
    
    Bang GetBang()
    {
        return Bang(100500);
    }
    
    int main()
    {
        Bang b;
        b = GetBang();
        b.Foo();
        return 0;
    }
    

    live demo

    Output:

    Bang(int i = 0) m_val address is 0x7ffd202c48b0
    Bang(int i = 0) m_val address is 0x7ffd202c48e0
    ~Bang()
    operator() m_val address is 0x7ffd202c48e0
    -1
    ~Bang()
    

    shows:

    • dtor will be called before output, That means that the temporary object has been destroyed.
    • m_value's address doesn't change.

    The two guaranteed we still access the temporary's m_value from the b's m_foo().

    It should be Undefined Behaviour to access an object which has be destroyed, but no warning and errors required.

    Update

    To solve the problem, there two solutions:

    1. Like @Killzone Kid points out, capture with an initializer: [bang = *this]. This requires c++14.
    2. More simpler way to capture of the current object by-copy: [*this]. This requires c++17. live demo