Использование 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()
звонили дважды... теперь это работает (удивительно для меня), но у меня есть оба, только чтобы показать, что я пытаюсь. Мои вопросы:
- Должен ли я использовать
std::move()
вset_callback()
функция, чтобы "вытащить" темп, и если я использую это действительно есть копия или делаетstd:: move(
) Значит, это не совсем копия? - Должен ли я использовать
std:: move()
пройти в лямбду... и это даже правильно. - Я думаю, я не понимаю, почему этот код работает с двумя
std:: moves()
... что означает, что я до сих пор не понимаю, чтоstd:: move()
делает - так что если кто-то может рассказать мне о том, что здесь происходит, это было бы здорово! - Я знаю, что могу передавать по значению, но моя цель состоит в том, чтобы сдвинуть временную копию, чтобы у меня не было ее копии. Это то, что подразумевается под идеальной пересылкой?
Мой пример можно увидеть здесь в 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
это просто приведение к значению.
- Вам не нужно использовать
std::move
внутри вашегоset_callback
метод.std::function
это CopyConstructible и CopyAssignable ( документы), так что вы можете просто написатьm_callback = callback;
, Если вы используетеstd::function
Переместите конструктор, объект, из которого вы переместились, будет "неопределенным" (но все еще действительным), в частности, он может быть пустым (что не имеет значения, потому что это временный объект). Вы также можете прочитать эту тему. - То же самое и здесь. Лямбда-выражение является временным и истекает после вызова
set_callback
, поэтому не имеет значения, переместите ли вы его или скопируете. - Код работает, потому что ваши обратные вызовы построены (двигаться построены) правильно. Нет причин для того, чтобы код не работал.
Ниже приведен пример с ситуацией, когда 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.