【问题标题】:Lambda Expression vs Functor in C++C++ 中的 Lambda 表达式与仿函数
【发布时间】:2011-01-13 23:29:18
【问题描述】:

我想知道我们应该在哪里使用 lambda 表达式而不是 C++ 中的仿函数。对我来说,这两种技术基本相同,甚至 functor 比 lambda 更优雅、更干净。例如,如果我想重用我的谓词,我必须一遍又一遍地复制 lambda 部分。那么 lambda 什么时候真正发挥作用呢?

【问题讨论】:

  • 顺便说一句,不知道我会把“lambda”作为代码。它不是 C++ 中的关键字或类型。有“lambda 表达式”之类的东西,但“lambda”不是你在代码中看到的东西,除非你使用 boost 的 lambda 库或者在你自己的代码中实际上有这样的东西。
  • @Noah Roberts:感谢您指出这一点。已编辑!我的意思是强调这个词。

标签: c++ lambda


【解决方案1】:

lambda 表达式创建一个无名函子,它是语法糖。

所以你主要使用它来让你的代码看起来更好。如果(a)你不打算重用仿函数,或者(b)你要重用它,但是从与当前代码完全无关的代码中,为了共享它你基本上会发生这种情况最终创建my_favourite_two_line_functors.h,并有不同的文件依赖它。

几乎与您键入任何代码行的条件相同,而不是将代码块抽象为函数。

也就是说,在 C++0x 中使用 range-for 语句,在某些地方你可能会使用仿函数,现在它可能会让你的代码看起来更好,将代码编写为循环体,而不是函子或 lambda。

【讨论】:

    【解决方案2】:

    1) 它是微不足道的,尝试分享它是工作多于收益。

    2) 定义函子只会增加复杂性(由于必须制作一堆成员变量和废话)。

    如果这些都不成立,那么也许你应该考虑定义一个函子。

    编辑:您似乎需要一个示例,说明何时最好在函子上使用 lambda。给你:

    typedef std::vector< std::pair<int,std::string> > whatsit_t;
    
    int find_it(std::string value, whatsit_t const& stuff)
    {
      auto fit = std::find_if(stuff.begin(), stuff.end(), [value](whatsit_t::value_type const& vt) -> bool { return vt.second == value; });
    
      if (fit == stuff.end()) throw std::wtf_error();
    
      return fit->first;
    }
    

    如果没有 lambda,您将不得不使用类似的东西来当场构造函子,或者为烦人的琐碎事情编写可外部链接的函子对象。

    顺便说一句,我想也许 wtf_error 是一个扩展。

    【讨论】:

    • 别忘了你的命名空间被仿函数类弄得乱七八糟。
    【解决方案3】:

    Lambdas 基本上只是实现函子的语法糖(注意:闭包并不简单。)在 C++0x 中,您可以使用 auto 关键字在本地存储 lambdas,并使用 std::function将使您能够存储 lambda,或以类型安全的方式传递它们。

    查看Wikipedia article on C++0x.

    【讨论】:

      【解决方案4】:

      不重复的小功能。

      对函子的主要抱怨是它们与使用它们的位置不同。因此,您必须在上下文之外找到并读取函子,以了解它正在使用的地方(即使它只在一个地方使用)。

      另一个问题是函子需要一些连线才能将参数输入函子对象。并不复杂,但所有基本的样板代码。而且样板容易出现剪切和粘贴问题。

      Lambda 尝试修复这两个问题。但是如果函数在多个地方重复或者大于(想不出一个合适的术语,因为它是上下文敏感的)小,我会使用函子。

      【讨论】:

        【解决方案5】:

        lambda 和仿函数有上下文。 Functor 是一个类,因此可能比 lambda 更复杂。函数没有上下文。

        #include <iostream>
        #include <list>
        #include <vector>
        using namespace std;
        
        //Functions have no context, mod is always 3
        bool myFunc(int n) { return n % 3 == 0; }
        
        //Functors have context, e.g. _v
        //Functors can be more complex, e.g. additional addNum(...) method
        class FunctorV
        {
           public:
           FunctorV(int num ) : _v{num} {}
        
           void addNum(int num) { _v.push_back(num); }
        
           bool operator() (int num)
           {
              for(int i : _v) {
                 if( num % i == 0)
                    return true;
              }
              return false;
           }
        
           private:
           vector<int> _v;
        };
        
        void print(string prefix,list<int>& l)
        {
           cout << prefix << "l={ ";
           for(int i : l)
              cout << i << " ";
           cout << "}" << endl;
        }
        
        int main()
        {
           list<int> l={1,2,3,4,5,6,7,8,9};
           print("initial for each test: ",l);
           cout << endl;
        
           //function, so no context.
           l.remove_if(myFunc);
           print("function mod 3: ",l);
           cout << endl;
        
           //nameless lambda, context is x
           l={1,2,3,4,5,6,7,8,9};
           int x = 3;
           l.remove_if([x](int n){ return n % x == 0; });
           print("lambda mod x=3: ",l);
           x = 4;
           l.remove_if([x](int n){ return n % x == 0; });
           print("lambda mod x=4: ",l);
           cout << endl;
        
           //functor has context and can be more complex
           l={1,2,3,4,5,6,7,8,9};
           FunctorV myFunctor(3);
           myFunctor.addNum(4);
           l.remove_if(myFunctor);
           print("functor mod v={3,4}: ",l);
           return 0;
        }
        

        输出:

        initial for each test: l={ 1 2 3 4 5 6 7 8 9 }
        
        function mod 3: l={ 1 2 4 5 7 8 }
        
        lambda mod x=3: l={ 1 2 4 5 7 8 }
        lambda mod x=4: l={ 1 2 5 7 }
        
        functor mod v={3,4}: l={ 1 2 5 7 }
        

        【讨论】:

          【解决方案6】:

          首先,我想在这里清除一些混乱。

          有两种不同的东西

          1. Lambda 函数
          2. Lambda 表达式/函子。

          通常,Lambda 表达式,即[] () {} -&gt; return-type 并不总是合成为闭包(即一种仿函数)。虽然这取决于编译器。但是您可以通过在[] 之前将+ 符号强制为+[] () {} -&gt; return-type 来强制编译器。这将创建函数指针。

          现在,来回答你的问题。您可以按如下方式重复使用 lambda:

          int main()
          {
              auto print = [i=0] () mutable {return i++;};
              cout<<print()<<endl;
              cout<<print()<<endl;
              cout<<print()<<endl;
              // Call as many time as you want
              return 0;
          }
          

          考虑到代码的表现力和易于维护性,您应该在想到任何地方使用 Lambda,就像您可以将它用于智能指针的自定义删除器和大多数 STL 算法一样。

          如果您将 Lambda 与 constexprvariadic template parameter packgeneric lambda 等其他功能结合使用。你可以实现很多事情。

          你可以找到更多关于它here

          【讨论】:

            【解决方案7】:

            正如您所指出的,当您需要一次性并且将其作为函数编写出来的编码开销不值得时,它的效果最好。

            【讨论】:

            • 感谢您的快速回复。但我不明白发明lamda 的动机是什么,因为我认为它更难阅读,而且它不可重复使用。它也没有名字,所以阅读代码需要更多时间。
            • @Chan:Herb Sutter 是 lambda 最直言不讳的倡导者之一。他发表了一些关于他们的谈话:herbsutter.com/2010/10/30/…。我自己没有看过它们,但它们无疑会告诉你认为它们有什么好处,并举例说明他什么时候会使用它们。
            【解决方案8】:

            从概念上讲,使用哪一个的决定与使用命名变量与就地表达式或常量的标准相同...

            size_t length = strlen(x) + sizeof(y) + z++ + strlen('\0');
            ...
            allocate(length);
            std::cout << length;
            

            ...在这里,创建一个长度变量会鼓励程序考虑它的正确性和意义,而将其隔离在以后的使用中。希望这个名称能够传达足够的信息,以便可以直观地理解它并且独立于它的初始值。然后它允许在不重复表达式的情况下多次使用该值(同时处理 z 不同)。在这里……

            allocate(strlen(x) + sizeof(y) + z++ + strlen('\0'));
            

            ...减少了总代码,并将值本地化在需要的位置。从这一行的阅读中“继承”的唯一事情是分配和增量(z)的副作用,但是没有额外的局部变量与范围或以后使用要考虑。程序员在继续分析代码的同时必须在心理上处理更少的状态。

            同样的区别也适用于函数和内联语句。为了回答您的问题,可以将仿函数与 lambda 视为此函数与内联决策的特例。

            【讨论】:

              【解决方案9】:

              这些天我更喜欢 Functors 而不是 Lambdas。尽管它们需要更多代码,但 Functor 会产生更简洁的算法。以下find_idfind_id2 之间的比较展示了该结果。虽然两者都产生了足够干净的代码,但find_id2 更容易阅读,因为MatchName(name) 的定义是从(并且次要于)主要算法中提取的。

              然而,我认为 Functor 代码应该放在函数定义正上方的实现文件中,用于提供对函数定义的直接访问。否则,Lambda 会更好地用于代码本地化/组织。

              #include <iostream>
              #include <vector>
              #include <string>
              using namespace std;
              
              struct Person {
                  int id;
                  string name;
              };
              
              typedef vector<Person> People;
              
              int find_id(string const& name, People const& people) {
                  auto MatchName = [name](Person const& p) -> bool
                  {
                      return p.name == name;
                  };
                  auto found = find_if(people.begin(), people.end(), MatchName);
                  if (found == people.end()) return -1;
                  return found->id;
              }
              
              struct MatchName {
                  string const& name;
                  MatchName(string const& name) : name(name) {}
                  bool operator() (Person const& person)
                  {
                      return person.name == name;
                  }
              };
              
              int find_id2(string const& name, People const& people) {
                  auto found = find_if(people.begin(), people.end(), MatchName(name));
                  if (found == people.end()) return -1;
                  return found->id;
              }
              
              int main() {
                  People people { {0, "Jim"}, {1, "Pam"}, {2, "Dwight"} };
                  cout << "Pam's ID is " << find_id("Pam", people) << endl;
                  cout << "Dwight's ID is " << find_id2("Dwight", people) << endl;
              }
              
              1. Functor 默认是自记录的;但是 Lambda 需要存储在更复杂的算法定义中的变量中(自我记录)。因此,最好不要像许多人那样使用 Lambda 的内联(为了代码可读性),以便获得上面MatchName Lambda 中所示的自我记录优势。

              2. 当 Lambda 存储在调用站点的变量中(或内联使用)时,主要算法会稍微难以阅读。由于 Lambda 在本质上是使用它们的算法的次要算法,因此最好通过使用自记录子例程(例如 Functor)来清理主要算法。这在本例中可能无关紧要,但如果想要使用更复杂的算法,它可以显着减少解释代码的负担。

              3. 函子可以很简单(如上面的示例)或复杂,因为它们需要。 有时复杂性是可取的,动态多态的情况下(例如策略/装饰器设计模式;或它们的模板等效策略类型)。这是 Lambda 无法满足的用例。

              4. Functors 需要明确声明捕获变量而不污染主要算法。当 Lambda 需要越来越多的捕获变量时,倾向于使用一揽子捕获喜欢[=]。但这大大降低了可读性,因为必须在 Lambda 定义和所有周围的局部变量(可能是成员变量等)之间跳跃。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2020-07-02
                • 1970-01-01
                • 2018-10-07
                • 2012-09-27
                • 1970-01-01
                相关资源
                最近更新 更多