10

我有以下(人为的)代码,其中我有一个带有单个打印函数的打印机类和一个处理字符串然后调用打印函数的回调函数的工作类:

#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. 我应该std::move()set_callback()函数中使用来“拉”出温度,如果我使用它,真的有一个副本还是std:: move() 意味着它不是真正的副本?
  2. 我应该std:: move()用来传递 lambda 吗……这是否正确。
  3. 我想我不明白为什么这段代码适用于两个std:: moves()......这意味着我仍然不明白std:: move()在做什么 - 所以如果有人能告诉我这里发生了什么,那就太好了!
  4. 我知道我可以按值传递,但我的目标是移动 temp 以便我没有它的副本。这就是完美转发的意思吗?

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

更新 我尝试使用 std::move 的原因是为了避免复制 lambda。(我认为这称为转发/完美转发)......但我认为我正在对其进行哈希处理!

4

3 回答 3

7

在这一行中,

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 还指出,虽然使用 pass-by-value-then-move-construct 进行初始化通常很好,但它不一定是assignment的最佳选择,因为 by-value 参数必须将内部内存分配给保持新状态,而当直接从const-qualified 引用函数参数复制时,它可以使用现有缓冲区。这以std::string参数为例,我不确定如何将其转移到std::function实例,但是当它们删除底层类型时,我可以想象这是一个问题,尤其是对于较大的闭包。

于 2018-09-13T07:36:34.957 回答
5

我想我不明白为什么这段代码适用于两个 std::moves... 这意味着我仍然不明白 std::move 在做什么

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

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

我应该使用 std::move 传递 lambda

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

我应该在 set_callback() 函数中使用 std::move

通过使用赋值运算符的移动版本到成员变量上,而不是常规赋值,并不是 100% 清楚你得到了什么。但是,它不会导致任何未定义的行为,因为您在移动参数后并没有尝试使用它。此外,由于 C++11 的参数 in将为右值(例如您的临时值)构造移动,并为左值构造复制,例如,如果您这样调用它:m_callbackcallbackset_callback()

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

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

我知道我可以按值传递,但我的目标是移动 temp 以便我没有它的副本(完美转发?)

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

template<class T>
void set_callback(T&& callback)
{
    m_callback = std::forward<T>(callback);
}
于 2018-09-13T07:27:15.773 回答
1

根据c ++,参考 std::move只是对右值参考的强制转换。

  1. 你不需要std::move在你的set_callback方法中使用。std::function是 CopyConstructible 和 CopyAssignable ( docs ),所以你可以只写m_callback = callback;. 如果您使用std::function移动构造函数,则您从中移动的对象将是“未指定的”(但仍然有效),特别是它可能是空的(这无关紧要,因为它是临时的)。您也可以阅读此主题
  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.
于 2018-09-13T07:41:27.767 回答