【问题标题】:Capturing lambda in std::function results in extra copies在 std::function 中捕获 lambda 会导致额外的副本
【发布时间】:2021-09-27 20:29:05
【问题描述】:

我正在尝试编写一些代码,以便通过将函数调用及其参数存储在 lambda/std::function 中来稍后调用函数。理想情况下,参数只会被复制一次(然后移动),但我能实现的最小副本数似乎是 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;
}

以上代码的输出为:

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

这对我来说毫无意义。 lambda 不应该捕获一次 Test 的实例,然后将该 lambda 一直转发到新的 std::function 从而导致移动吗?

如果我这样修改队列函数,我至少可以摆脱一次副本。

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

输出:

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

但我仍然不明白多余的副本是从哪里来的。

谁能帮我解惑?

【问题讨论】:

  • 你在使用Visual Studio吗?
  • @TedLyngmo 不,我只是在使用 gcc(8.1.0 版)
  • 好吧,奇怪。我在 VS 上写了一份故障报告(上面的链接),因为我发现它在一个 std::function 的信号分配上复制了 1 次 + 移动了 4 次。对于 gcc 和 clang,我找到了 1 个副本 + 1 个移动。如果您在我的链接中运行程序,您的 gcc 会得到什么?我刚刚注意到最新的 gcc 只管理一个副本,没有移动。
  • @TedLyngmo 我得到以下输出: Foo() Foo() -- 构造 func -- Foo(const Foo&) Foo(Foo&&) ~Foo() 1 -- 分配给 func -- Foo (const Foo&) Foo(Foo&&) ~Foo() 2 ~Foo() 3 -- 程序结束 -- ~Foo() 4 ~Foo() 5 ~Foo() 6 Foo 析构函数调用了 6 次
  • 好的,复制分配时1复制+1移动。这和我得到的一样。如果你移动分配,我猜你会得到 2 次移动。他们似乎在最新的 gcc 中对此进行了改进。现在只完成一个副本或一个移动。

标签: c++ lambda std-function


【解决方案1】:

AFAICT 问题出在 foo() 参数的 const 上。当您在foo(const Test&amp; t) 中捕获t 时,lambda 中的捕获类型也是const。稍后当您转发 lambda 时,lambda 的移动构造函数将别无选择,只能复制而不是移动捕获。您不能从const 移动。 将 foo 更改为 foo(Test&amp; t) 后,我得到:

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

https://stackoverflow.com/a/31485150/85696 中提到的替代解决方案是使用 [t=t] 形式的捕获。

通过移动捕获和其他两个更改,也可以消除这个剩余的复制构造函数:

- 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

【讨论】:

  • 想象一下 lambda 和队列不存在,而 foo 只是调用 _foo。不应有任何 Test 副本,因为传递了 const 引用。我的目标是仍然说参数是 const 引用,但必须为队列制作副本(即,队列是中间步骤,但函数将自行调用)。这不可能吗?
  • 我刚刚对 lambdas 做了一些研究。在这种情况下,将 lambda 声明为“可变”会有所帮助吗?
  • Mutable 仅影响 lambda 的 operator(),使其成为非常量。要使成员成为非 const,您可以使用 [t=t] 形式的捕获。我更新了答案。
猜你喜欢
  • 2013-12-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-24
  • 2014-06-24
  • 1970-01-01
  • 1970-01-01
  • 2020-05-29
相关资源
最近更新 更多