У меня есть следующий (надуманный) код, в котором у меня есть класс принтера с единственной функцией печати в нем и рабочий класс, который обрабатывает строку, а затем вызывает функцию обратного вызова для функции печати:
#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, заключалась в том, чтобы избежать копирования лямбда. (Я думаю, что это называется пересылкой / идеальной пересылкой) ... но я думаю, что придумываю хеш!
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
предназначен для того, чтобы сделать это явным, если вы намереваетесь использовать двигаться из для объекта. Семантика перемещения предназначены для работы с rvalues. Таким образом, std::move()
принимает любое выражение (например, lvalue) и делает из него rvalue. Это использование обычно возникает, когда вам нужно разрешить передачу lvalue перегрузкам функций, которые принимают ссылка rvalue в качестве аргумента, например, переместить конструкторы и переместить операторы присваивания. Идея движущийся заключается в эффективной передаче ресурсов вместо создания копий.
В вашем фрагменте вы не используете std::move()
недопустимым образом, следовательно, этот код работает. В оставшейся части ответа мы пытаемся понять, выгодно ли это использование или нет.
Should I use std::move to pass in the lambda
Похоже, нет, у вас нет причин делать это во фрагменте. Прежде всего, вы звоните move()
по тому, что уже является rvalue. Кроме того, синтаксически set_callback()
получает свой аргумент std::function<bool(std::string)>
по значению, из которого ваша лямбда в настоящее время отлично инициализирует экземпляр.
Should I use std::move in the set_callback() function
Не на 100% ясно, что вы получаете, используя версию двигатьсяоператор присваивания для переменной-члена m_callback
вместо обычного присваивания. Однако это не вызовет неопределенного поведения, поскольку вы не пытаетесь использовать аргумент после его перемещения. Кроме того, начиная с C++ 11, параметр callback
в set_callback()
будет движение построено для rvalues, такой как ваш временный, и копия, созданная для lvalue, например, если бы вы назвали его так:
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.
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?)
В контексте вычет типа (template
или auto
) T&&
- это ссылка на пересылку, а не ссылка rvalue. Таким образом, вам нужно написать функцию только один раз (функция шаблона, без перегрузок), и внутренняя зависимость от std::forward
(эквивалент static_cast<T&&>
) гарантирует, что в любом варианте использования описанный выше путь для использования двух перегрузок сохраняется в условия стоимости, являющиеся назначением копии для вызова lvalue и назначением перемещения для вызова rvalue:
template<class T>
void set_callback(T&& callback)
{
m_callback = std::forward<T>(callback);
}
Я просто чувствую, что первый пункт недостаточно конкретизирован, чтобы я мог принять решение. Я действительно думаю, что здесь есть место для обсуждения плюсов и минусов перемещения и копирования, как это делает lubgr. Мне кажется, что ваше третье утверждение противоречит первому. callback
есть и lvalue, так что сделать из него rvalue и украсть его внутренности, похоже, в любом случае имеет смысл на первый взгляд.
@SkepticalEmpiricist Я был бы очень признателен, если бы вы могли рассказать что-нибудь о том, как должна выполняться идеальная переадресация ... Я думаю, что я просто ошибся здесь, судя по звуку этого :)
@SkepticalEmpiricist, спасибо, что потрудился объяснить это для меня :). Я думаю (из того, что вы сказали), что я мог бы просто заставить set_callback принимать auto &&fn
в качестве параметра (чтобы спасти меня от перехода на полный шаблон), потому что (как вы упомянули) оба используют вывод типа ... это работает? (выбрано в качестве ответа из-за времени, потраченного на объяснение :)
@SkepticalEmpiricist: да, но я также имел в виду, что вы потратили время на объяснение "лишней" информации, которая не входила строго в исходный вопрос :)
@code_fodder Это совершенно нормально. Если вас устраивает, я добавлю "лишний" вопрос к исходному вашему сообщению, чтобы он был виден при первом взгляде.
@SkepticalEmpiricist на самом деле это хорошая идея ... Я, вероятно, просто плохо сформулировал свой вопрос, поэтому, пожалуйста, сделайте :)
В этой строке
w.set_callback( std::move([&](std::string s){ return p.print(s); }) );
вы приводите rvalue к rvalue. Это запретная операция и, следовательно, бессмысленная. Передача временного значения функции, которая принимает свой параметр по значению, по умолчанию допустима. Аргумент функции, скорее всего, так или иначе будет создан на месте. В худшем случае он создается с помощью перемещения, что не требует явного вызова 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 в Эфф. Современный C++. Здесь, однако, Мейерс также указывает, что, хотя обычно нормально использовать конструкцию передачи по значению, затем перемещение для инициализация, это не обязательно лучший вариант для назначение, потому что параметр по значению должен выделять внутреннюю память для сохранить новое состояние, в то время как он может использовать существующий буфер при прямом копировании из параметра ссылочной функции, квалифицированного const
. Это проиллюстрировано для аргументов std::string
, и я не уверен, как это можно передать экземплярам std::function
, но поскольку они стирают базовый тип, я мог представить себе, что это проблема, особенно для больших замыканий.
Вы правы в том, что копирование иногда лучше, чем переезд для выполнения заданий, но ваши рассуждения - нет. Причина в том, что параметр по значениюдолжен создает буфер для копии содержимого (которое затем будет перемещено в LHS назначения), где содержимое const &
должно быть скопировано, но буфер LHS может быть использован повторно. IOW, потенциально ненужное выделение происходит в параметре, а не в LHS назначения.
@Angew Спасибо за подсказку, это именно то, что я пытался сказать, позвольте мне уточнить.
@lubgr, хорошее описание, спасибо. Только один вопрос в вашем комментарии // Now it makes sense to cast the function to an lvalue (don't use it afterwards!)
, вы имеете в виду rvalue ref
вместо lvalue
, или я не понял?
ДА!! Это довольно тупой тип. Спасибо, что заметили это!
Согласно справочник по C++std::move
- это просто приведение к ссылке rvalue.
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.
Ваш пункт 1 звучит некорректно. std::vector
также поддерживает функцию CopyAssignable, но есть огромная разница между копированием и перемещением. Даже std::function
, скорее всего, будет содержать динамически выделяемую память, поэтому ее перемещение вместо копирования может сэкономить (дорого!) Выделение.
@Angew, да, вы правы, копирование может быть дороже, чем перемещение (а может и не быть), однако я действительно сомневаюсь, что вы легко заметите разницу в приложении. Я исследовал созданный ассемблерный код здесь, разница в том, что конструктор перемещения использует swap
(но это может зависеть от реализации).
@ Ptaq666 В данном случае это может не иметь значения, но ОП явно пытается понять общие принципы. Сделайте вызываемый объект достаточно большим, чтобы он не вписался в оптимизацию малого буфера std::functions
, а затем сравните сборку.
@SkepticalEmpiricist Я знаю, что могу передавать по значению, но моей целью было переместить временную шкалу так, чтобы у меня не было ее копии (идеальная пересылка?) .. Я не объяснил это четко.