使用 std::move 传递临时 lambda,或使用 "pull" 传递临时参数,有什么区别?

Using std::move to pass in a temp lambda, or to "pull" out a temp parameter and what is the difference?

我有以下(设计的)代码,其中我有一台打印机 class,其中有一个打印功能和一个工作的 class,它处理一个字符串,然后调用回调函数打印函数:

#include <functional>
#include <iostream>

using callback_fn = std::function<bool(std::string)>;

class printer
{
public:   
    bool print(std::string data)
    {
        std::cout << data << std::endl;
        return true;
    }
};

class worker
{
public:   
    callback_fn m_callback;
    void set_callback(callback_fn callback)
    {
        m_callback = std::move(callback);  // <-- 1. callback is a temp, so what does std::move do here?
    }
    void process_data(std::string data)
    {
        if (!m_callback(data)) { /* do error handling */ }
    }
};

int main() {
    printer p;
    worker w;

    w.set_callback( std::move([&](std::string s){ return p.print(s); }) ); // <-- 2. what does std::move do here?
    w.process_data("hello world2");
}

注意: 我已经 std:: move() 调用了两次...现在这有效(令我感到惊讶),但我都只是为了展示我正在尝试的东西。我的问题是:

  1. 我应该在 set_callback() 函数中使用 std::move() 来 "pull" 输出温度吗?这不是真正的副本?
  2. 我应该使用 std:: move() 来传递 lambda 吗……这是否正确。
  3. 我想我不明白为什么这段代码与两个 std:: moves() 一起工作......这意味着我仍然不明白 std:: move() 在做什么 - 所以如果有人能启发我关于这里发生的事情那就太好了!
  4. 我知道我可以按值传递,但我的目标是移动临时文件以便我没有它的副本。 完美转发是什么意思?

我的示例可以在 wandbox 中看到:https://wandbox.org/permlink/rJDudtg602Ybhnzi

更新 我尝试使用 std::move 的原因是为了避免复制 lambda。 (我认为这叫做 forwarding/perfect-forwarding)...但我认为我正在对它进行哈希处理!

I guess I don't understand why this code works with two std::moves... which implies that I still don't understand what std::move is doing

std::move 是为了在您打算 对象移动的情况下使其明确。 移动语义 旨在与右值 一起使用。因此,std::move() 接受任何表达式(例如左值)并从中生成 rvalue。当您需要允许将 lvalue 传递给接受 rvalue reference 作为参数的函数重载时,通常会出现这种用法,例如 移动构造函数 移动赋值运算符移动 的想法是有效地传输资源而不是复制。

在您的代码段中 您没有以无效的方式使用 std::move(),因此此代码有效。在剩下的答案中,我们尝试看看这种用法是否有利。

Should I use std::move to pass in the lambda

似乎不​​,您没有理由在代码段中这样做。首先,你在已经是 rvalue 上调用 move()。此外,从语法上讲,set_callback() 正在按值接收其 std::function<bool(std::string)> 参数,其中您的 lambda 目前正在初始化一个实例。

Should I use std::move in the set_callback() function

使用 move 版本的 赋值运算符 m_callback 成员变量,而不是常规赋值。但是,它不会导致任何未定义的行为,因为您不会在移动参数后尝试使用它。此外,由于 C++11 set_callback() 中的 callback 参数将 move constructed for rvalues 例如你的临时,并为 lvalue 构造副本,例如如果您这样称呼它:

auto func = [&](std::string s){ return p.print(s); };
w.set_callback(func);

您需要考虑的是在方法内部,在您的情况下移动是否比复制更好。移动涉及它自己的 移动赋值的实现 相关类型。我不只是在这里说 QOI,而是考虑到在移动时你需要释放任何资源 m_callback 一直保持到那个点,并且对于从构造实例移动的场景(正如我们已经介绍的 callback 已经从其参数复制构造或移动构造),这增加了该构造已经具有的成本。不确定这样的移动开销是否适用于您的情况,但您的 lambda 复制起来并不明显昂贵。也就是说,选择两种重载,一种采用 const callback_fn& callback 并在内部进行复制分配,另一种采用 callback_fn&& callback 并在内部进行移动分配,可以完全缓解这一潜在问题。在任何一个中,您都不需要为参数构造任何东西,总的来说,您不一定会释放旧资源作为开销,因为在执行复制分配时,可以通过复制到 LHS 上来潜在地使用它们现有的资源而不是在从 RHS 移动之前释放它。

I know that I can pass by value, but my goal was to move the temp so that I don't have a copy of it (perfect forwarding?)

类型推导templateauto)的上下文中,T&&forwarding reference,不是右值引用。因此,您只需编写一次函数(模板函数,无重载),并且在内部依赖 std::forward(相当于 static_cast<T&&>)将确保在任何用例中,上述路径使用这两个重载的成本是 lvalue 调用的复制赋值和 rvalue 调用的移动赋值:

template<class T>
void set_callback(T&& callback)
{
    m_callback = std::forward<T>(callback);
}

这一行,

w.set_callback( std::move([&](std::string s){ return p.print(s); }) );

您将右值转换为右值。这是空操作,因此毫无意义。默认情况下,将临时对象传递给按值接受其参数的函数是可以的。无论如何,函数参数很可能就地实例化。在最坏的情况下,它是移动构造的,不需要在函数参数上显式调用 std::move - 同样,因为它在您的示例中已经是右值。为了澄清情况,请考虑这种不同的情况::

std::function<bool(std::string)> lValueFct = [&](std::string s){ /* ... */ }

// Now it makes sense to cast the function to an rvalue (don't use it afterwards!)
w.set_callback(std::move(lValueFct));

现在来看另一种情况。在这个片段中

void set_callback(callback_fn callback)
{
    m_callback = std::move(callback);
}

你移动赋值给 m_callback。这很好,因为参数是按值传递的,以后不会使用。关于此技术的一个很好的资源是 Eff 中的第 41 项。现代 C++。在这里,Meyers 还指出,然而,虽然为 初始化 采用按值传递然后移动构造通常很好,但它不一定是 [=26] 的最佳选择=]assignment,因为按值参数必须分配内部内存来保存新状态,而直接从 const 限定的引用函数参数复制时可以使用现有缓冲区。这是 std::string 参数的示例,我不确定如何将其转移到 std::function 实例,但是当它们擦除底层类型时,我可以想象这是一个问题,尤其是对于较大的闭包.

根据 c++ reference std::move 只是对右值引用的转换。

  1. 您不需要在 set_callback 方法中使用 std::movestd::function 是 CopyConstructible 和 CopyAssignable (docs), so you can just write m_callback = callback;. If you use std::function move constructor, the object you moved from will be "unspecified" (but still valid), in particular it might be empty (which doesn't matter because it is a temporary). You can also read this topic.
  2. 这里也是一样。 Lambda 表达式是临时的,在调用 set_callback 后就过期了,所以移动或复制它都没有关系。
  3. 代码有效,因为您的回调构建(移动构建)正确。代码没有理由不起作用。

以下是 std::move 产生影响的情况示例:

callback_fn f1 = [](std::string s) {std::cout << s << std::endl; return true; };
callback_fn f2 = f1;
f1("xxx");  // OK, invokes previously assigned lambda
f2("xxx");  // OK, invokes the same as f1
f2 = std::move(f1);
f1("xxx");  // exception, f1 is unspecified after move

输出:

xxx
xxx
C++ exception with description "bad_function_call" thrown in the test body.