Использование std::move для передачи временной лямбды или для "извлечения" временного параметра, и в чем разница?

У меня есть следующий (надуманный) код, где у меня есть класс принтера с единственной функцией печати и рабочим классом, который обрабатывает строку и затем вызывает функцию обратного вызова для функции печати:

#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() пройти в лямбду... и это даже правильно.
  3. Я думаю, я не понимаю, почему этот код работает с двумя std:: moves()... что означает, что я до сих пор не понимаю, что std:: move() делает - так что если кто-то может рассказать мне о том, что здесь происходит, это было бы здорово!
  4. Я знаю, что могу передавать по значению, но моя цель состоит в том, чтобы сдвинуть временную копию, чтобы у меня не было ее копии. Это то, что подразумевается под идеальной пересылкой?

Мой пример можно увидеть здесь в wandbox: https://wandbox.org/permlink/rJDudtg602Ybhnzi

ОБНОВЛЕНИЕ Причина, по которой я пытался использовать std::move, заключалась в том, чтобы не копировать лямбду. (Я думаю, что это называется пересылкой / совершенной пересылкой)... но я думаю, что я делаю из этого хеш!

3 ответа

Решение

Я думаю, я не понимаю, почему этот код работает с двумя std::move... что означает, что я до сих пор не понимаю, что делает std::move

std::move чтобы сделать это явным в тех случаях, когда вы собираетесь перейти от объекта. Семантика Move предназначена для работы со значениями. Таким образом, std::move() берет любое выражение (например, lvalue) и делает из него rvalue. Такое использование обычно возникает, когда необходимо разрешить передачу lvalue в перегрузки функций, которые принимают ссылку на rvalue в качестве параметра, например конструкторы перемещения и операторы присваивания перемещения. Идея перемещения состоит в том, чтобы эффективно передавать ресурсы, а не делать копии.

В вашем фрагменте вы не используете std::move() неверным образом, следовательно, этот код работает. В остальной части ответа мы пытаемся понять, является ли это использование выгодным или нет.

Должен ли я использовать std::move для перехода в лямбду

Кажется, нет, у вас нет причин делать это во фрагменте. Прежде всего, вы звоните move() на то, что безусловно уже является ценным. Далее, синтаксически, set_callback() получает его std::function<bool(std::string)> аргумент по значению, из которого ваша лямбда инициализирует экземпляр просто отлично в настоящее время.

Должен ли я использовать std::move в функции set_callback()

Не ясно на 100%, что вы получаете, используя версию перемещения оператора присваивания на m_callback переменная-член, вместо обычного присваивания. Это не приведет к неопределенному поведению, поскольку вы не пытаетесь использовать аргумент после его перемещения. Кроме того, так как C++11 callback параметр в set_callback() будет создан для значений r, таких как ваше временное значение, и для элементов, созданных для значения l, например, если вы назвали бы его так:

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

Что нужно учитывать, так это то, лучше ли внутри метода перемещение, чем копирование в вашем случае. Перемещение включает в себя собственную реализацию назначения перемещения для соответствующего типа. Я не просто говорю здесь QOI, но учту, что при переезде вам нужно освободить любой ресурс m_callback держался до этого момента, и для сценария перехода от созданного экземпляра (как мы уже говорили, callback была либо построена как копия, либо как построена на основе ее аргумента), это добавляет к стоимости, которую уже имела эта конструкция. Не уверен, что такие движущиеся издержки применимы в вашем случае, но тем не менее ваша лямбда не так уж и дорога для копирования, как есть Тем не менее, выбирая две перегрузки, один принимает const callback_fn& callback и копирование присваивание внутри и один взяв callback_fn&& callback и назначение движения внутрь позволило бы смягчить эту потенциальную проблему в целом. Как и в любом из них, вы ничего не создаете для параметра, и в целом вы не обязательно высвобождаете старые ресурсы в качестве накладных расходов, как при выполнении копирования-назначения, вы можете потенциально использовать уже существующие ресурсы LHS, копируя на них вместо того, чтобы выпустить его до перемещения тех из RHS.

Я знаю, что могу передавать по значению, но моя цель состояла в том, чтобы переместить временную копию, чтобы у меня не было ее копии (идеальная пересылка?)

В контексте вывода типа (template или же auto), T&& ссылка на пересылку, а не ссылка на значение. Таким образом, вы должны написать функцию только один раз (шаблонная функция, без перегрузок) и полагаться на нее 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 на аргумент функции - опять же, так как это уже rvalue в вашем примере. Чтобы прояснить ситуацию, рассмотрим этот другой сценарий:

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, Это нормально, так как параметр передается по значению и впоследствии не используется. Один хороший ресурс по этой технике - это пункт 41 в Eff. Современный C++. Здесь Мейерс также указывает, однако, что хотя обычно хорошо использовать конструкцию pass-by-value-then-move-construct для инициализации, это не обязательно лучший вариант для назначения, поскольку параметр by-value должен выделять внутреннюю память для держать новое состояние, в то время как это может использовать существующий буфер при прямом копировании из constпараметр эталонной функции. Это является примером для std::string аргументы, и я не уверен, как это можно передать std::function экземпляры, но поскольку они стирают базовый тип, я мог представить, что это проблема, особенно для больших замыканий.

Согласно справке C++ std::move это просто приведение к значению.

  1. Вам не нужно использовать std::move внутри вашего set_callback метод. std::function это CopyConstructible и CopyAssignable ( документы), так что вы можете просто написать m_callback = callback;, Если вы используете std::function Переместите конструктор, объект, из которого вы переместились, будет "неопределенным" (но все еще действительным), в частности, он может быть пустым (что не имеет значения, потому что это временный объект). Вы также можете прочитать эту тему.
  2. То же самое и здесь. Лямбда-выражение является временным и истекает после вызова 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.
Другие вопросы по тегам