Использование std :: move для передачи временной лямбды или «извлечения» временного параметра, и в чем разница? - PullRequest
0 голосов
/ 13 сентября 2018

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

#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 ]

0 голосов
/ 13 сентября 2018

В этой строке

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

вы приводите значение к значению. Это неоперация и, следовательно, бессмысленно. Передача временного значения в функцию, которая принимает ее параметр по значению, подходит по умолчанию. Аргумент функции, скорее всего, будет создан в любом случае. В худшем случае это конструкция с перемещением, которая не требует явного вызова std::move в аргументе функции - опять же, поскольку это уже значение r в вашем примере. Чтобы прояснить ситуацию, рассмотрим этот другой сценарий ::

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-затем-move-для инициализации , это не обязательно лучший вариант для назначений потому что параметр по значению должен выделять внутреннюю память для хранения нового состояния, в то время как он мог бы использовать существующий буфер при прямом копировании из const -качественного параметра эталонной функции. Это иллюстрируется для std::string аргументов, и я не уверен, как это можно перенести в std::function экземпляры, но, поскольку они стирают базовый тип, я мог представить, что это проблема, особенно для больших замыканий.

0 голосов
/ 13 сентября 2018

Согласно 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.
0 голосов
/ 13 сентября 2018

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

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

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

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

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

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

Не на 100% ясно, что вы получаете, используя переместить версию оператора присваивания в переменную-член m_callback вместо обычного присваивания. Это не приведет к неопределенному поведению, поскольку вы не пытаетесь использовать аргумент после его перемещения. Кроме того, поскольку в C ++ 11 параметр callback в set_callback() будет сконструирован для перемещения для значений r , например, для вашего временного, и для копии, созданной для 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.

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

В контексте вывода типа (template или auto) T&& представляет собой ссылку для пересылки , а не значение r * значение, Таким образом, вы должны написать функцию только один раз (шаблонная функция, без перегрузок), и полагаясь на std::forward (эквивалент static_cast<T&&>), убедитесь, что в любом случае использования описанный выше путь для использования двух перегрузки сохраняются с точки зрения стоимости, являющейся назначением копирования для вызова lvalue и назначением перемещения для вызова rvalue :

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