Patrick Wright
Patrick Wright

Reputation: 1663

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?

Upvotes: 2

Views: 559

Answers (1)

danadam
danadam

Reputation: 3450

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

Upvotes: 6

Related Questions