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

标签 c++ c++14

我仍然对完善的前进和前进的引用书不甚了解。我试图了解在传递lambda表达式方面的差异。我的假设是我将使用std::function<..>auto接受lambda函数类型,但是然后查看Folly source code,我发现它们正在使用函数模板。

我编写了一个小型测试程序来尝试理解左值和右值的差异,但看不到任何差异。下面的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;
}

作为引用,输出为:
test1
test0
test2
test0
test3
test0
test4
test0
test6
test0
test7
test0

最佳答案

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

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);
}

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

在讨论lambda和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

我将逐一介绍并解释为什么它们会按照自己的方式行事:
  • SetObject:此函数接受forwarding reference。与std::forward结合使用时,这些保留了传递给它们的对象的值类别。这意味着,当我们调用SetObject(object)时,object从中进行复制分配,当我们调用SetObject(std::move(object))时,object从中进行移动分配。
  • SetObject2:该函数接受转发引用,但是由于您没有使用std::forward保留参数的值类别,因此它始终是左值,因此是从中复制分配的。
  • SetObject3:此函数按值接受其参数。参数对象可以根据传递给函数的对象的值类别进行复制或移动构造,但是由于参数对象是左值,因此始终对其进行复制分配。
  • SetObject3Variant:与SetObject3一样,此函数也按值接受其参数,并且根据传递给该函数的对象的值类别对参数对象进行复制或移动构造。然后,我们使用std::move将参数对象转换为右值,从而使其从中进行移动分配,而不是进行复制分配。
  • SetObject4:该函数的工作方式与SetObject3完全相同。 auto参数只是模板的语法糖。
  • SetObject4Variant:此函数的工作原理与SetObject3Variant
  • 完全一样
  • SetObject5:此函数通过对非常量的左值引用来接受其参数。那些只能绑定(bind)到左值,因此您根本不能将右值传递给它。由于它是一个左值,因此它的参数将被复制分配。
  • SetObject6:这与SetObject2完全一样。同样,auto参数只是模板的语法糖。
  • SetObject6Variant:它与SetObject完全一样,但是std::forward语法有点儿奇怪,因为您没有要引用的显式模板类型参数。
  • SetObject7:此函数按值接受ShowMeHolder。将根据传递给它的const ShowMe&对象的值类别,使用ShowMe&&ShowMe构造函数构造该对象。然后,由于参数是左值,因此该函数将ShowMeHolder参数对象复制分配给类成员。
  • SetObject7Variant:此功能与SetObject7相似,但由于已使用std::move将参数对象强制转换为右值,因此它是从移动对象分配的。


  • 将其放回lambda,所有操作均完全相同。只需用一些lambda类型替换ShowMe,用ShowMeHolder替换std::function。这两种类型都没有什么特别的。 Lambda只是带有重载的operator()的对象,而std::function只是一个包含其他一些类似于函数的对象的对象(使用大量技巧可以存储任何类型的类似于函数的对象)。

    关于c++ - 传递lambda函数的权衡是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59762298/

    相关文章:

    c++ - 非终止 constexpr 函数是否定义明确?

    c++ - inpaint() 没有产生预期的结果。为什么?

    c++ - 使用可变参数模板解决 C++ 中的 mixin 构造函数问题

    c++ - 自动功能必须在使用前定义

    c++ - 函数模板重载解析困惑

    C++14 堆栈分配的共享指针

    c++ - 对于不确定值的无符号字符类型,左值到右值转换标准中特殊语言的意义是什么

    c++ - 是否可以根据基于范围的 for 类型调用不同的取消引用运算符重载

    c++ - 有没有办法重载 << 以对某个类特化执行空操作?

    C++ 如果这些函数在构造函数中传递,我如何使用带有自定义散列和比较的 unordered_map 作为成员变量?