传递 lambda 函数的权衡是什么?

dro*_*del 3 c++ c++14

我仍然没有很好地掌握完美的前进和移动参考。我试图了解在传递 lambda 表达式方面的差异。我的假设是我会使用std::function<..>auto接受 lambda 函数类型,但是查看Folly 源代码后,我发现他们正在使用函数模板。

我写了一个小测试程序来尝试理解左值和右值的差异,但我看不到任何差异。SetLambda*()下面的变体之间有什么区别吗?

据我所知,唯一不起作用的是SetLambda5()在给定左值时。作为参考,我使用的是支持 C++14 的 GCC 版本。

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;
}
Run Code Online (Sandbox Code Playgroud)

并且,作为参考,输出:

test1
test0
test2
test0
test3
test0
test4
test0
test6
test0
test7
test0
Run Code Online (Sandbox Code Playgroud)

Mil*_*nek 5

当接受一个未知的函子时,你将直接调用它而不存储它,理想的、保留值类别的方法是:

template <typename Func>
void DoAThing(Func&& func) {
    std::forward<Func>(func)(parameters);
}
Run Code Online (Sandbox Code Playgroud)

当您想将函子存储在一个std::function对象中以便稍后调用时,只需接受 astd::function并让隐式转换为您完成大部分工作:

void StoreAFunctor(std::function<void()> func) {
    myFunctor = std::move(func);
}
Run Code Online (Sandbox Code Playgroud)

在深入解释之前,首先要提到的是,使用移动语义和完美转发的目的是避免进行昂贵的复制操作。我们希望在可能的情况下转移资源的所有权,而不是不必要地复制它们。如果您的对象不拥有任何可移动资源(如没有捕获的 lambda 的情况),那么这些都不重要。只需通过对常量的引用传递对象并根据需要复制它。如果您的对象确实拥有一些可移动资源,那么事情就会变得棘手。

在讨论 lambdas 和 之前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; }
};
Run Code Online (Sandbox Code Playgroud)

我还将使用这种简单类型作为以下内容的替代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;
};
Run Code Online (Sandbox Code Playgroud)

使用该类型,这是一个重现所有测试用例(加上一些变体)的示例:

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);
}
Run Code Online (Sandbox Code Playgroud)

这给出了以下输出:

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
Run Code Online (Sandbox Code Playgroud)

现场演示

我将逐一介绍并解释为什么他们的行为方式如此:

  • SetObject:此函数接受转发引用。当与std::forward这些结合时,保留传递给它们的对象的值类别。这意味着当我们调用时SetObject(object) object获取复制分配自,当我们调用时SetObject(std::move(object)) object获取移动分配自。
  • SetObject2:此函数接受转发引用,但由于您没有使用std::forward来保留参数的值类别,因此它始终是左值,因此是从中复制分配的。
  • SetObject3:此函数按值接受其参数。参数对象是基于传递给函数的对象的值类别的复制或移动构造的,但是参数对象始终是从其复制分配的,因为它是左值。
  • SetObject3Variant:这个函数,就像SetObject3按值接受它的参数一样,参数对象是根据传递给函数的对象的值类别复制或移动构造的。然后std::move,我们使用将参数对象强制转换为右值,使其被移动分配而不是复制分配。
  • SetObject4:此功能的工作原理与SetObject3. 该auto参数只是模板的语法糖。
  • SetObject4Variant:这个功能的工作原理完全一样 SetObject3Variant
  • SetObject5:这个函数通过左值引用到非常量来接受它的参数。那些只能绑定到左值,因此您根本无法将其传递给右值。由于它是左值,因此它的参数被复制分配。
  • SetObject6: 这与SetObject2. 同样,auto参数只是模板的语法糖。
  • SetObject6Variant:这完全一样,SetObject只是std::forward语法有点不稳定,因为您没有要引用的显式模板类型参数。
  • SetObject7: 这个函数接受一个ShowMeHolderby 值。该对象将根据传递给它的对象的值类别使用const ShowMe&ShowMe&&构造函数构造ShowMe。该函数然后将ShowMeHolder参数对象复制分配给类成员,因为参数是左值。
  • SetObject7Variant:此函数的工作方式与 类似SetObject7,但参数对象是从 移动分配的,因为它使用 被强制转换为右值std::move

把它带回 lambdas,一切都完全一样。只需更换ShowMe一些λ型和ShowMeHolderstd::function。这两种类型都没有什么特别之处。Lambda 只是带有重载的对象operator(),并且std::function只是一个包含其他一些类函数对象的对象(使用一堆技巧来存储任何类型的类函数对象)。