Reputation: 1663
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
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