Каковы компромиссы при передаче лямбда-функций? - PullRequest
3 голосов
/ 16 января 2020

У меня до сих пор нет отличного гр asp совершенных ссылок вперед и движения. Я пытаюсь понять различия в отношении передачи лямбда-выражений. Я предполагал, что я буду использовать std::function<..> или auto для принятия лямбда-типов функций, но затем, глядя на исходный код Folly , я вижу, что они используют шаблоны функций.

Я написал небольшую тестовую программу, чтобы попытаться понять различия, как с lvalue, так и с rvalues, но я их не вижу. Есть ли различия между SetLambda*() вариантами ниже?

Насколько я могу судить, единственное, что не работает, это SetLambda5(), когда ему дается lvalue. Для справки я использую версию G CC с поддержкой C ++ 14.

struct MyClass {
  template<typename Lambda>
  void SetLambda(Lambda&& lambda) { mLambda = std::forward<Lambda>(lambda); }

  template<typename Lambda>
  void SetLambda2(Lambda&& lambda) { mLambda = lambda; }

  template<typename Lambda>
  void SetLambda3(Lambda lambda) { mLambda = lambda; }

  void SetLambda4(auto lambda) { mLambda = lambda; }
  //void SetLambda5(auto& lambda) { mLambda = lambda; }
  void SetLambda6(auto&& lambda) { mLambda = lambda; }
  void SetLambda7(std::function<void()> lambda) { mLambda = lambda; }

  void Run() { mLambda(); }
  std::function<void()> mLambda;
};

int main() {
  auto lambda = []() { std::cout << "test0\n"; };

  MyClass myClass;
  myClass.SetLambda([]() { std::cout << "test1\n"; });
  myClass.Run();
  myClass.SetLambda(lambda);
  myClass.Run();
  myClass.SetLambda2([]() { std::cout << "test2\n"; });
  myClass.Run();
  myClass.SetLambda2(lambda);
  myClass.Run();
  myClass.SetLambda3([]() { std::cout << "test3\n"; });
  myClass.Run();
  myClass.SetLambda3(lambda);
  myClass.Run();
  myClass.SetLambda4([]() { std::cout << "test4\n"; });
  myClass.Run();
  myClass.SetLambda4(lambda);
  myClass.Run();
  //myClass.SetLambda5([]() { std::cout << "test5\n"; });
  //myClass.Run();
  //myClass.SetLambda5(lambda);
  //myClass.Run();
  myClass.SetLambda6([]() { std::cout << "test6\n"; });
  myClass.Run();
  myClass.SetLambda6(lambda);
  myClass.Run();
  myClass.SetLambda7([]() { std::cout << "test7\n"; });
  myClass.Run();
  myClass.SetLambda7(lambda);
  myClass.Run();

  return 0;
}

И, для справки, вывод:

test1
test0
test2
test0
test3
test0
test4
test0
test6
test0
test7
test0

1 Ответ

5 голосов
/ 16 января 2020

Принимая неизвестный функтор, который вы собираетесь вызывать напрямую, не сохраняя его, идеальный способ сохранения категории значений - это:

template <typename Func>
void DoAThing(Func&& func) {
    std::forward<Func>(func)(parameters);
}

Когда вы хотите сохранить функтор в std::function объект для вызова позже, просто примите std::function и позвольте неявному преобразованию сделать большую часть работы за вас:

void StoreAFunctor(std::function<void()> func) {
    myFunctor = std::move(func);
}

Прежде чем углубляться в более глубокое объяснение, первым делом Следует отметить, что смысл использования семантики перемещения и идеальной пересылки - избегать дорогостоящих операций копирования. Мы хотим переместить владение ресурсами, когда это возможно, вместо ненужного их копирования. Если ваши объекты не владеют какими-либо подвижными ресурсами (как в случае лямбды без захвата), то все это не имеет значения. Просто передайте объект по ссылке на const и скопируйте его по мере необходимости. Если ваши объекты имеют некоторые подвижные ресурсы, то все становится волосатым.

Прежде чем говорить о лямбдах и std::function, я собираюсь сделать шаг назад и посмотреть, как все работает с этим простым типом, который показывает, что происходит:

struct ShowMe {
  ShowMe() { }
  ShowMe(const ShowMe&) { std::cout << "ShowMe copy constructed\n"; }
  ShowMe(ShowMe&&) { std::cout << "ShowMe move constructed\n"; }
  ShowMe& operator=(const ShowMe&) { std::cout << "ShowMe copy assigned\n"; return *this; }
  ShowMe& operator=(ShowMe&&) { std::cout << "ShowMe move assigned\n"; return *this; }
};

Я также собираюсь использовать этот простой тип в качестве замены для std::function:

struct ShowMeHolder {
  ShowMeHolder() { }
  ShowMeHolder(const ShowMe& object) : mObject{object} { }
  ShowMeHolder(ShowMe&& object) : mObject{std::move(object)} { }
  ShowMeHolder& operator=(const ShowMe& object) { mObject = object; return *this; }
  ShowMeHolder& operator=(ShowMe&& object) { mObject = std::move(object); return *this; }

  ShowMe mObject;
};

Использование этого Тип, вот пример, который воспроизводит все ваши тестовые случаи (плюс несколько вариантов):

struct MyClass {
  template<typename Object>
  void SetObject(Object&& object) { mObject = std::forward<Object>(object); }

  template<typename Object>
  void SetObject2(Object&& object) { mObject = object; }

  template<typename Object>
  void SetObject3(Object object) { mObject = object; }

  template <typename Object>
  void SetObject3Variant(Object object) { mObject = std::move(object); }

  void SetObject4(auto object) { mObject = object; }
  void SetObject4Variant(auto object) { mObject = std::move(object); }
  void SetObject5(auto& object) { mObject = object; }
  void SetObject6(auto&& object) { mObject = object; }
  void SetObject6Variant(auto&& object) { mObject = std::forward<decltype(object)>(object); }
  void SetObject7(ShowMeHolder object) { mObject = object; }
  void SetObject7Variant(ShowMeHolder object) { mObject = std::move(object); }

  ShowMeHolder mObject;
};

int main() {
  MyClass myClass;
  ShowMe object;

  std::cout << "SetObject move\n";
  myClass.SetObject(std::move(object));
  std::cout << "SetObject copy\n";
  myClass.SetObject(object);

  std::cout << "SetObject2 move\n";
  myClass.SetObject2(std::move(object));
  std::cout << "SetObject2 copy\n";
  myClass.SetObject2(object);

  std::cout << "SetObject3 move\n";
  myClass.SetObject3(std::move(object));
  std::cout << "SetObject3 copy\n";
  myClass.SetObject3(object);

  std::cout << "SetObject3Variant move\n";
  myClass.SetObject3Variant(std::move(object));
  std::cout << "SetObject3Variant copy\n";
  myClass.SetObject3Variant(object);

  std::cout << "SetObject4 move\n";
  myClass.SetObject4(std::move(object));
  std::cout << "SetObject4 copy\n";
  myClass.SetObject4(object);

  std::cout << "SetObject4Variant move\n";
  myClass.SetObject4Variant(std::move(object));
  std::cout << "SetObject4Variant copy\n";
  myClass.SetObject4Variant(object);

  //std::cout << "SetObject5 move\n";
  //myClass.SetObject5(std::move(object));
  std::cout << "SetObject5 copy\n";
  myClass.SetObject5(object);

  std::cout << "SetObject6 move\n";
  myClass.SetObject6(std::move(object));
  std::cout << "SetObject6 copy\n";
  myClass.SetObject6(object);

  std::cout << "SetObject6Variant move\n";
  myClass.SetObject6Variant(std::move(object));
  std::cout << "SetObject6Variant copy\n";
  myClass.SetObject6Variant(object);

  std::cout << "SetObject7 move\n";
  myClass.SetObject7(std::move(object));
  std::cout << "SetObject7 copy\n";
  myClass.SetObject7(object);

  std::cout << "SetObject7Variant move\n";
  myClass.SetObject7Variant(std::move(object));
  std::cout << "SetObject7Variant copy\n";
  myClass.SetObject7Variant(object);
}

Это дает следующий вывод:

SetObject move
ShowMe move assigned
SetObject copy
ShowMe copy assigned
SetObject2 move
ShowMe copy assigned
SetObject2 copy
ShowMe copy assigned
SetObject3 move
ShowMe move constructed
ShowMe copy assigned
SetObject3 copy
ShowMe copy constructed
ShowMe copy assigned
SetObject3Variant move
ShowMe move constructed
ShowMe move assigned
SetObject3Variant copy
ShowMe copy constructed
ShowMe move assigned
SetObject4 move
ShowMe move constructed
ShowMe copy assigned
SetObject4 copy
ShowMe copy constructed
ShowMe copy assigned
SetObject4Variant move
ShowMe move constructed
ShowMe move assigned
SetObject4Variant copy
ShowMe copy constructed
ShowMe move assigned
SetObject5 copy
ShowMe copy assigned
SetObject6 move
ShowMe copy assigned
SetObject6 copy
ShowMe copy assigned
SetObject6Variant move
ShowMe move assigned
SetObject6Variant copy
ShowMe copy assigned
SetObject7 move
ShowMe move constructed
ShowMe copy assigned
SetObject7 copy
ShowMe copy constructed
ShowMe copy assigned
SetObject7Variant move
ShowMe move constructed
ShowMe move assigned
SetObject7Variant copy
ShowMe copy constructed
ShowMe move assigned

Live Demo

Я прохожу go через каждого и объясню, почему они ведут себя так, как они:

  • SetObject: эта функция принимает ссылку на пересылку, В сочетании с std::forward они сохраняют категорию значений объекта, переданного им. Это означает, что когда мы вызываем SetObject(object) object получает назначение копирования от, а когда мы вызываем SetObject(std::move(object)) object получает назначение перемещения от.
  • SetObject2: эта функция принимает ссылку на пересылку, но, поскольку вы не использовали std::forward для сохранения категории значений параметра, это всегда lvalue, и, следовательно, присваивается копия из.
  • SetObject3: эта функция принимает свой параметр по значению. Объект параметра либо копируется, либо перемещается в зависимости от категории значения объекта, переданного в функцию, но затем объект параметра всегда назначается для копирования, так как это lvalue.
  • SetObject3Variant: Это функция, как и SetObject3, принимает свой параметр по значению, а объект параметра либо копируется, либо перемещается, сконструированный на основе категории значения объекта, переданного функции. Затем мы используем std::move для приведения объекта параметра к значению, в результате чего ему присваивается перемещение, а не назначение копирования.
  • SetObject4: эта функция работает точно так же, как SetObject3. Параметр auto является просто синтактическим c сахаром для шаблона.
  • SetObject4Variant: эта функция работает точно так же, как SetObject3Variant
  • SetObject5: эта функция принимает свой параметр путем именующий-ссылка-к-неконстантному. Они могут быть привязаны только к l-значению, поэтому вы не можете передать это значение. Его параметру присваивается копия, так как это lvalue.
  • SetObject6: Это работает точно так же, как SetObject2. Опять же, параметры auto - это просто синтаксис c сахар для шаблонов.
  • SetObject6Variant: Это работает точно так же, как SetObject, за исключением того, что синтаксис std::forward немного неудобен, так как у вас нет явный параметр типа шаблона для ссылки.
  • SetObject7: эта функция принимает значение ShowMeHolder по значению. Этот объект будет создан с использованием конструктора const ShowMe& или ShowMe&& на основе категории значений переданного ему объекта ShowMe. Затем функция копирует-присваивает объект параметра ShowMeHolder члену класса, поскольку параметр является lvalue.
  • SetObject7Variant: эта функция работает аналогично SetObject7, но объект параметра назначается при перемещении с тех пор, как он был приведен к rvalue с использованием std::move.

Если вернуть его в lambdas, все работает точно так же. Просто замените ShowMe лямбда-типом и ShowMeHolder на std::function. В этих типах нет ничего особенного. Лямбды - это просто объекты с перегруженным operator(), а std::function - это просто объект, который содержит какой-то другой функционально-подобный объект (используя кучу трюков, чтобы иметь возможность хранить любой функционально-подобный тип объект).

...