C++中的匿名函数(译)

99封情书 提交于 2020-02-29 17:21:47

    C++11最令人兴奋的特性之一就是能够创建匿名函数(lambda functions),有时也被称为闭包(closures)。这意味着什么?lambda function是一个可以内联写在代码里的函数(通常被传递给另一个函数,与函数对象(functor)和函数指针有相似的概念)。有了lambda,快速创建函数已经变得更容易,也意味着不仅可以在之前需要定义一个独立的命名函数时使用lambda function,而且也可以凭借快速创建简单的函数写出更多的代码。本文中我会先解释为什么lambda是很棒的,并会给出一些例子,然后简略阐述使用lambda时你所可以做的所有细节。

为什么lambdas让人震撼

    假使有一个地址簿类,而且想要提供一个搜索方法。你可以提供一个简单的搜索函数,接受一个string作为参数并返回所有与该string参数相匹配的地址。有时是该类的用户所想要的,但要是他们仅想在域名中搜索,或更可能在用户名中搜索且忽略掉在域名中的结果呢?或许他们可能想要搜索所有同时出现在另一个列表中的邮件地址,有许多潜在地待搜索的感兴趣的事情。代替把这些所有的选择都构建到类里,岂不是更好提供一个接受确定邮件地址是否是感兴趣的过程作为参数的通用find方法?我先称该方法为findMatchingAddresses,且接受一个函数或类函数参数。

  1. #include <string>
  2. #include <vector>
  3. class AddressBook {
  4. public:
  5.     // using a template allows us to ignore the differences between functors, functon
  6.     // pointers and lambda
  7.     template<typename Func>
  8.     std::vector<std::string> findMatchingAddresses(Func func) {
  9.         std::vector<std::string> results;
  10.         for (auto iter = _addresses.begin(), end = _addresses.end(); iter != end; iter++) {
  11.         // call the function passed into findMatchingAddresses and see if it matches
  12.         if (func(*iter)) {
  13.             results.push_back(*iter);
  14.         }
  15.     }
  16.     return results;
  17. }
  18. private:
  19.     std::vector<std::string> _addresses;
  20. };

    任何人都可以都可以传递一个函数给findMatchingAddresses,该函数是一个包含查找逻辑的特殊函数。当给出一个特定地址时,如果该函数返回true,则返回该地址。在早期的C++版本中这种方式是可以的,但是有一个严重的缺点:不能方便的创建函数。你需要在一个地方定义,然后为了一次简单的使用要是它能够被传递进来。那就是lambas出现的地方。

基本lambda语法

在写代码解决该问题之前,先看一下lambda基本的语法。

  1. #include <iostream>
  2. using namespace std;
  3. int main()
  4. {
  5.     auto func = []() { cout << "Hello world" << endl; };
  6.     func();   // now call the function
  7. }

    注意到了以[]开始的lambda了吗?那个标识符被称为捕获说明(capture specification),用于告诉编译器穿件一个lambda function。你将会看到它(或一个变种)在每一个lambda function的开始处。

    接下来,像其他的函数一样有一个参数列表:()。返回值在哪?事实说明不需要给出一个返回值。在C++11中,如果编译器可以推断lambda function的返回值,它将会进行推断而不需要你给出一个返回值。在这个例子里,编译可以知道函数无返回值。接着就是函数体,输出“hello world”。然后这行代码实际上不会输出任何东西,这里仅是创建了一个函数。几乎像定义一个普通的函数,仅仅是内联余下的代码。

    在仅接下来的一行调用lambda function: func(),这看起来和调用其他任何函数一样。顺便地注意auto使其变得多么的容易。你不再需要对函数指针这种丑陋的语法而烦恼。

使用lambda

    让我们先看下怎样应该它到我们的地址簿的例子中,首先创建一个简单的函数,用于查找包含.org的邮件地址。

  1. Addressbook global_address_book;
  2. vector<string> findAddressesFromOrgs()
  3. {
  4.         return global_address_book.findMatchingAddresses(
  5.                 // we're declaring a lambda here; the [] signals the start
  6.                 [](const string &addr) { return addr.find(".org") != string::npos; }
  7.         );
  8. }

    再次,我们以捕获标识符(capture specifier)[]开始,但这次我们有一个参数--地址,检查它是否包含".org"。仍然在lambda function的函数体内没有什么会被执行;它仅仅是在findMatchingAddresses中,当func被使用时,lambda function中的代码会执行。

    也就是说,通过findMatchingAddresses的每次循环,都会调用lambda function且给它一个地址作为参数,然后检查是否含有".org"。

lambdas的变量捕获

    尽管lambda这些简单的使用是吸引人的,但变量捕获(variable capture)才是真正的秘密武器(secret sauce),它使得lambda function变得强大。假使你想要创建一个查找包含特定名字的地址的小函数,是不是会更好把代码写长像这样?

  1. // read in the name from a user, which we want to search
  2. string name;
  3. cin >> name;
  4. return global_address_book.findMatchingAddresses(
  5.         // notice that the lambda function uses the variable 'name'
  6.         [&](const string& addr) { return addr.find(name) != string::npos; }
  7. );

    事实上这个例子是完全合法的,而且它展现出lambda的真正价值。 我们可以接受一个lambda外的变量声明(name),而在lambda内使用它。当findMatchingAddresses调用我们的lambda function时,在其内部的所有代码执行,当addr.find被调用时,它可以访问用户传入的name。为使它起作用所需要做的唯一的事就是让编译器知道我们所想要的变量捕获。我所做的就是使用[&]进行捕获说明,而不是[]。[]告诉编译器不需要捕获任何变量,而[&]告诉编译器执行变量捕获。

    是不是很不可思议?我们可以创建一个带有捕获的变量名的简单函数,将它传递给find方法,所有的仅写在一行代码里。没有C++11要得到相似的行为,我们或者需要创建一个函数对象,或者在类AddressBook中需要一个特定的方法。在C++11中,我们可以有一个单一简单的接口相对于AddressBook,它可以真正简单地支持任何种类的过滤。

    只为好玩(just for fun),假如说我们想要查找一个邮件地址,其长度不长于某个给定的字符数。可以简单地这样做:

  1. int min_len = 0;
  2. cin >> min_len;
  3. return global_address_book.findMatchingAddresses([&](const string &addr) { return addr.length() >= min_len;});

    顺便说一下,从Herb Sutter盗用的,你应该习惯看到(you should get used to seeing)“});”。这是标准的lambda函数结尾语法,你看到和在自己代码里使用lambda越多,你越会看到越多的小部分语法。

Lambda和STL

lambda functions的最大受益者之一毫无疑问标准模板库算法包的大量用户。先前,使用像for_each这样的算法是很别扭的实践。现在几乎可以像写一个普通循环一样使用for_each和其它STL算法。相对比:

  1. vector<int> v;
  2. v.push_back(1);
  3. v.push_back(2);
  4. for (auto it = v.begin(), end = v.end(); it != end; it++) {
  5.         cout << *it;
  6. }

  1. vector<int> v;
  2. v.push_back(1);
  3. v.push_back(2);
  4. for (auto it = v.begin(), end = v.end(); it != end; [](int val) {
  5.         cout << val;
  6. });

    代码开看起来很漂亮,读起来像是被结构化的普通循环,突然你就能够使用for_each在普通循环之上提供的优势,如保证有正确的结束条件。你可能想知道是否会影响性能?很重要的事(here's the kicker):for_each与普通循环有相同的性能,有时甚至更高(因为它可以利用循环展开(loop unrolling)的优势)。

    如果想了解更多的C++11 lambda和STL的益处,你将会喜欢Herb Sutter讨论C++11的视频。

    我希望这个STL的例子能够向你展现出lambda function不仅仅是能简单的创建函数--它们允许你完全形成写程序的新方式,像在那些把其它函数作为数据和允许提取出特殊数据结构进行处理的代码。for_each作用在列表上和作用在树上有相似的函数岂不是很好,所需要做的就是确定将要处理的每个节点是正常的节点,而不需要担心遍历算法?这种分解一个函数处理数据结构和另一个函数被委托处理数据的方式是很强大的。有了lambda,C++可以使用这种方式编程。当然不是以前不能这样做,for_each算法也并不是新的,而仅仅是以前不想这样做。

更多Lambda的语法

    顺便一说,如果函数没有参数,参数列表像返回值一样是可选的。最简短的lambda表达式可能如下:[]{}

这样一个函数不接受任何参数也不做任何事情。仅略微复杂一点的例子:

  1. using namespace std;
  2. #include <iostream>
  3. int main()
  4. {
  5.  
  6.         [] { cout << "Hello, my Greak friends"; } ();
  7. }

个人而言,我还是不接受忽略掉参数列表,我认为[]()结构更容易使lambda functions在代码中比较显著,而时间会告诉人们提出什么时标准。

返回值

    如果lambda没有返回值则默认是void,如果有一个简单的return表达式,则编译器会推断出返回值类型。

  1. []() { return 1; }   // compiler knows this returns an integer

    对于较复杂的lambda functions,并且有多个返回值,则需要你指定返回值类型。(某些编译器,像GCC,即使有多个返回值,如果没有指定返回类型也会让你通过,但是标准没有对此做出保证。)

    lambda可以利用C++11新的返回值语法把返回值放到函数之后。事实上如果想要为lambda functions指定返回值也必须这样做。这是一个上边例子显示指定返回值的版本:

  1. []() -> int { return 1; }    // now we're telling the compiler what we want

异常说明

    尽管C++委员会决定废弃异常说明(除了我后边文章会涉及的几个例子),但他们并没有从语言中移除,有些静态代码分析工具需要检查异常说明,像PCLint。如果你使用这些工具中的某一种来做编译时异常检查,你真的是想要能够说出你的lambda抛出哪种异常。我可以看到这样做的主要原因是你传递一个lambda作为另一个函数的参数且函数期望lambda抛出仅指定异常集合中的某个异常。通过为lambda functions提供一个异常说明,你可以让PCLint这样的工具来为你那种情况的检查。事实上只要你想你就可以这样做。有个不接受参数也不抛出异常的lambda例子:

  1. []() throw() { /* code that you don't expect to throw an exception*/ }

怎样实现Lambda闭包

    变量捕获的魔力怎样真正起作用的?事实上lambda是通过创建一个小的类的方法实现的,这个类重载了operator(),以便看起来就像是一个函数。lambda function就是该类的一个实例;当类被创建好后,周围环境内的任何变量都会被传递给lambda function class的构造函数,并保存为成员变量。事实上与函数对象的概念有点类似。C++11的优势使这样做起来变得很容易--所以可以随时使用它,而不是仅仅在那些罕见的情况下要写一个完整的有意义的新类。

    鉴于对性能的敏感,C++对于捕获什么变量和怎样捕获给出了很大的灵活性,所有都通过捕获说明[]进行控制。前面已经看过两种情况--[]不捕获任何变量和[&]以引用捕获所有的变量。如果你用一个空捕获说明创建lambda,C++会创建一个普通函数而不是创建一个类。以下是捕获选项列表:

  1. []                    不捕获变量(or,a scorched earch strategy?)
  2. [&]                 以引用方式捕获变量
  3. [=]                  以值方式捕获变量
  4. [=,&foo]         以值方式捕获变量,除了以引用方式捕获变量foo
  5. [bar]               仅捕获变量bar
  6. [this]               捕获包围类的this指针

    注意最后的捕获选项--如果指定了=或&,就默认捕获了this指针就不需要再包含它,然而可以捕获一个函数的this指针是相当重要的,因为这意味着在写lambda functions时不需要区分局部变量和类的成员变量,两者都可以访问。你不需要显示使用this指针,就像是在写内联函数,多么酷的一件事。

  1. class Foo {
  2. public:
  3.         Foo() : _x(3) {}
  4.         void func() {
  5.                 // a very silly, but illustrative way of printing out the value of _x
  6.                 [this]() { cout << _x; }();
  7.         }
  8. private:
  9.         int _x;
  10. };
  11. int main()
  12. {
  13.         Foo f;
  14.         f.func();
  15. }

引用捕获的风险和益处

    在使用引用捕获时,lambda functions能够修改lambda之外的局部变量--毕竟是引用。但这同时也意味着如果从一个函数返回一个lambda function,你就不能使用引用捕获,因为在函数返回后所引用的变量将是无效的。

Lambda是什么类型?

我们已经看到可以使用模板来接受一个lambda function作为参数,而auto也可以把一个lambda function作为局部变量。但应该怎样命名一个特定的lambda呢?由于每个lambda function都是通过创建一个单独的类实现的,正如之前所见,甚至单个lambda function也是不同的类型--即使两个函数有相同的参数和返回值!但是C++11包含了一个方便的包装器(wrapper),可以存储任何类型的函数--lambda function,functor,function pointer: std::function。

std::function

这个新的std::function是一种很棒的方式,用于传递lambda function作为参数和返回值。它允许在模板中指定确切的参数类型和返回值类型。AddressBook 的例子,这次使用std::function代替模板,注意需要包含functional头文件:

  1. #include <functional>
  2. #include <vector>
  3. class AddressBook {
  4. public:
  5.          std::vector<string> findMatchingAddresses (std::function<bool (const string&)> func) {
  6.                  std::vector<string> results;
  7.                  for ( auto itr = _addresses.begin(), end = _addresses.end(); itr != end; ++itr ) {
  8.                          // call the function passed into findMatchingAddresses and see if it matches
  9.                          if ( func( *itr ) ) {
  10.                                  results.push_back( *itr );
  11.                          }
  12.                  }
  13.                  return results;
  14.              }
  15. private:
  16.         std::vector<string> _addresses;
  17. };

    std::function相对于模板最大的优势是写一个模板需要把整个函数放到头文件中,而std::function不需要。这可能会真的有些帮助如果你工作在有许多需要待修改的代码而且还被包含在许多源文件中。

    如果想要检查std::function类型的变量是否拥有一个有效函数,可以将其像一个boolean类型一样对待:

  1. std::function<int ()> func;
  2. // check if we have a function (we don't since we didn't provide one)
  3. if (func) {
  4.         // if we did have a function, call it
  5.         func();
  6. }

关于函数指针的注记

    根据最终的C++11标准,如果lambda的捕获说明是空的,那么它可以被看作为一个普通函数且可以赋值给一个函数指针。下面是一个无捕获lambda使用函数指针的例子:

  1. typedef int (*func)();
  2. func f = []() -> int { return 2; };
  3. f();

    一个无捕获的lambda不需要属于自己的类,它可以被编译成一个普通的老式函数,允许它像一个一般的函数被传递。MSVC10不支持。

用lambdas创建委托

看一个更复杂的lambda function例子--这次创建一个委托。什么是委托?在调用普通函数时,你所需要的就是函数自身。当在一个对象上调用方法时,你需要两个东西:函数和对象本身。func()和obj.method()是不同的。调用一个方法,你需要两个,仅仅传递一个方法的地址给函数时不够的,需要在一个对象上调用方法。

看例子,以期望一个函数作为参数的代码,在里面我们会传递一个委托:

  1. #include <functional>
  2. #include <string>
  3. class EmailProcessor {
  4. public:
  5.         void receiveMessage(const std::string &message) {
  6.                 if (_handler_func) {
  7.                         _handler_func(message);
  8.                 }
  9.                 // other processing
  10.         }
  11.         void setHandlerFunc(std::function<void (const std::string&)> handler_func) {
  12.                 _hanlder_func = handler_func;       
  13.         }
  14. private:
  15.         std::function<void (const std::string&)> _handler_func;
  16. };

这是很标准的模型,允许一个回调函数被注册到一个类中。

而现在我们想要另一个类负责跟踪目前收到的最长消息。总之,我们可以像这样创建一个小类:

  1. #include <string>
  2. class MessageSizeStore {
  3. public:
  4.         MessageSizeStore() : _max_size(0) {}
  5.         void checkMessage(const std::string& message) {
  6.                 const int size = message.length();
  7.                 if (size > _max_size) {
  8.                         _max_size = size;
  9.                 }
  10.         }
  11.         int getSize() { return _max_size; }
  12. private:
  13.         int _max_size;
  14. };

    应如何让checkMessage方法无论何时有消息到达都被调用?我们不能仅传递checkMessage本身--它是一个方法,需要一个对象。

  1. EmailProcessor processor;
  2. MessageSizeStore size_store;
  3. processor.setHandlerFunc(checkMessage);  // this won't work

    我们需要某种方式绑定变量size_store到被传递给setHandlerFunc的函数。听起来像是lambda的工作!

  1. EmailProcessor processor
  2. MessageSizeStore size_store;
  3. processor.setHandlerFunc([&](const std::string& message) {
  4.         size_store.checkMessage(message);
  5. });

是不是很酷?我们使用lambda function作为胶水代码,传递给了setHandlerFunc一个普通函数,还是在方法之上做了一个调用--在C++中创建了一个简单的委托。

结论

这门语言已经存在了几十年,lambda function会真的开始到处出现在C++代码中吗?我认为是的,我已经在产品级代码中使用lambda,而且他们也在到处出现--在某些缩减代码的情况,某些改进单元测试的情况和替换先前使用宏所完成的情况。我认为lambda比任何希腊字母都让人震撼。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!