Search code examples
c++lambdastd-function

Capturing lambda in std::function results in extra copies


I am trying to write some code which allows me to call a function at some later time by storing the function call and its arguments in a lambda/std::function. Ideally, the arguments would only be copied ONCE (and moved oterhwise) but the smallest number of copies I can achieve seems to be 2.

//==============================================================================
// INCLUDES
//==============================================================================

#include <iostream>
#include <functional>
#include <memory>

//==============================================================================
// VARIABLES
//==============================================================================

static std::unique_ptr<std::function<void()>> queueFunction;

//==============================================================================
// CLASSES
//==============================================================================

class Test {
public:
    Test(int a, int b = 20, int c = 30) : _a(a), _b(b), _c(c) {
        std::cout << "Test: Constructor" << std::endl;
    }
    
    ~Test() {
        std::cout << "Test: Destructor" << std::endl;
    }
    
    Test(const Test& other) :
        _a(other._a)
    {
        std::cout << "Test: Copy Constructor" << std::endl;
    }
    
    Test(Test&& other) :
        _a(std::move(other._a))
    {
        std::cout << "Test: Move Constructor" << std::endl;
    }
    
    Test& operator=(const Test& other) {
        if (this != &other) {
            _a = other._a;
        
            std::cout << "Test: Assignment Operator" << std::endl;
        }
        
        return *this;
    }
    
     Test& operator=(Test&& other) {
        if (this != &other) {
            _a = std::move(other._a);
        
            std::cout << "Test: Move Assignment Operator" << std::endl;
        }
        
        return *this;
    }
    
    friend std::ostream& operator<<(std::ostream& os, const Test& v) {
        os << "{a=" << v._a << "}";
        return os;
    }
    
private:
    int _a;
    int _b;
    int _c;
};

//==============================================================================
// FUNCTIONS
//==============================================================================

void foo(const Test& t);
void _foo(const Test& t);

template <typename F>
void queue(F&& fn) {
    std::cout << "queue()" << std::endl;
    
    queueFunction = std::make_unique<std::function<void()>>(std::forward<F>(fn));
}

void dequeue() {
    std::cout << "dequeue()" << std::endl;
    
    if (queueFunction) {
        (*queueFunction)();
    }
    
    queueFunction.reset();
}

void foo(const Test& t) {
    std::cout << "foo()" << std::endl;
    
    queue([t](){
       _foo(t); 
    });
    
    //Only a single copy of Test is made here
    /*
    [t](){
       _foo(t); 
    }();
    */
}

void _foo(const Test& t) {
    std::cout << "_foo()" << std::endl;
    std::cout << "t=" << t << std::endl;
}


//==============================================================================
// MAIN
//==============================================================================

int main() {
    std::cout << "main()" << std::endl;
    
    Test test1(20);
    
    foo(test1);
    dequeue();
    
    std::cout << "main() return" << std::endl;
    
    return 0;
}

The output of the above code is:

main()
Test: Constructor
foo()
Test: Copy Constructor
queue()
Test: Copy Constructor
Test: Copy Constructor
Test: Destructor
Test: Destructor
dequeue()
_foo()
t={a=20}
Test: Destructor
main() return
Test: Destructor

Which makes no sense to me. Shouldn't the lambda capture the instance of Test once, then forward that lambda all the way to the new std::function thus causing a move?

If I modify my queue function as such I can at least get rid of once copy.

void queue(std::function<void()> fn) {
    std::cout << "queue()" << std::endl;
    
    queueFunction = std::make_unique<std::function<void()>>(std::move(fn));
}

Output:

main()
Test: Constructor
foo()
Test: Copy Constructor
Test: Copy Constructor
queue()
Test: Destructor
dequeue()
_foo()
t={a=20}
Test: Destructor
main() return
Test: Destructor

But I still cannot understand where the extra copy is coming from.

Can someone help to enlighten me?


Solution

  • AFAICT the problem is the const of the foo() argument. When you capture t inside foo(const Test& t), then the type of that capture inside the lambda is also const. Later when you forward the lambda, the lambda's move constructor will have no choice but copy, not move, the capture. You cannot move from const. After changing foo to foo(Test& t) I get:

    main()
    Test: Constructor
    foo()
    Test: Copy Constructor
    queue()
    Test: Move Constructor
    Test: Move Constructor
    Test: Destructor
    Test: Destructor
    dequeue()
    _foo()
    t={a=20}
    Test: Destructor
    main() return
    Test: Destructor
    

    Alternative solution, mentioned in https://stackoverflow.com/a/31485150/85696, is to use capture in the form [t=t].

    With move-capture and two other changes it is possible to eliminate this remaining copy constructor too:

    - void foo(const Test& t) {
    + void foo(Test t) {
    ...
    -    queue([t](){
    +    queue([t =  std::move(t)](){
    ...
    -    foo(test1);
    +    foo(std::move(test1));
    
    main()
    Test: Constructor
    Test: Move Constructor
    foo()
    Test: Move Constructor
    queue()
    Test: Move Constructor
    Test: Move Constructor
    Test: Destructor
    Test: Destructor
    Test: Destructor
    dequeue()
    _foo()
    t={a=20}
    Test: Destructor
    main() return
    Test: Destructor