C ++ 11中的Lambda函数 Lambda Functions in C++11 - the Definitive Guide

4 篇文章 0 订阅
 

λ   C ++ 11最令人兴奋的功能之一就是能够创建lambda函数(有时称为闭包)。这是什么意思?lambda函数是可以在源代码中内联编写的函数(通常传递给另一个函数,类似于仿函数 或函数指针的概念)。使用lambda,创建快速函数变得更加容易,这意味着您不仅可以在以前需要编写单独的命名函数时开始使用lambda,而且可以开始编写更多依赖于创建功能的代码快速简便的功能。在本文中,我将首先通过一些示例解释lambda为何如此出色,然后再逐步介绍lambda可以做什么的所有细节。

Why Lambdas Rock

假设您有一个通讯录类,并且希望能够提供搜索功能。您可能提供一个简单的搜索功能,获取一个字符串并返回与该字符串匹配的所有地址。有时候,这就是该类用户想要的。但是,如果他们只想在域名中搜索,或者更可能只在用户名中搜索,而忽略域名中的结果怎么办?或者,也许他们想搜索也在另一个列表中显示的所有电子邮件地址。有很多可能有趣的东西要寻找。不是将所有这些选项都构建到类中,而是提供一种通用的“ find”方法,该方法采用一个过程来确定电子邮件地址是否有趣,这不是很好吗?让我们调用方法findMatchingAddresses,并让其采用“函数”或“

#include <string>
#include <vector>

类AddressBook
{
    上市:
    //使用模板可让我们忽略函子,函数指针之间的差异 
    //和lambda
    template <typename Func>
    std :: vector <std :: string> findMatchingAddresses(Func函数)
    { 
        std :: vector <std :: string>结果;
        对于(auto itr = _addresses.begin(),end = _addresses.end(); itr!= end; ++ itr)
        {
            //调用传递给findMatchingAddresses的函数,看看是否匹配
            如果(func(* itr))
            {
                results.push_back(* itr);
            }
        }
        返回结果;
    }

    私人的:
    std :: vector <std :: string> _addresses;
};

任何人都可以将函数传递到findMatchingAddresses中,该函数包含用于查找特定函数的逻辑。如果函数返回true,则在给定特定地址时,将返回该地址。这种方法在C ++的早期版本中是可以的,但是它存在一个致命的缺陷:创建函数不够方便。您必须在其他地方定义它,以便能够将其传递给一种简单的用法。那就是lambda进来的地方。

基本Lambda语法

在编写一些代码来解决此问题之前,让我们看一下lambda的真正基本语法。

#include <iostream>

使用名称空间std;

int main()
{
    auto func = [](){cout <<“ Hello world”; };
    func(); //现在调用函数
}

OK,您会发现以[]开头的lambda吗?该标识符称为捕获规范,它告诉编译器我们正在创建lambda函数。您会在每个lambda函数的开头看到此(或变体)。

接下来,像其他任何函数一样,我们需要一个参数列表:()。返回值在哪里?事实证明,我们不需要付出任何代价。在C ++ 11中,如果编译器可以推断出lambda函数的返回值,它将执行此操作,而不是强制您将其包括在内。在这种情况下,编译器知道该函数不返回任何内容。接下来,我们将打印出“ Hello World”的正文。这行代码实际上并不会导致任何输出-我们只是在这里创建函数。几乎就像定义一个普通函数一样-它恰好与其余代码内联。

我们仅在下一行调用lambda函数:func()-就像调用任何其他函数一样。顺便说一下,请注意使用auto多么容易!您无需费心使用函数指针的丑陋语法。

在我们的示例中应用Lambda

让我们看看如何将其应用于我们的地址簿示例,首先创建一个简单的函数来查找包含“ .org”的电子邮件地址。

AddressBook global_address_book;

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

我们再次从捕获说明符[]开始,但这次我们有一个参数——地址,我们检查它是否包含“.org”。再一次,这个lambda函数的主体中还没有执行任何内容;当使用变量func时,它只在findmatchingaddress中执行lambda函数中的代码。换句话说,每个循环通过findmatchingaddress,它调用lambda函数并将地址作为参数提供给它,函数检查它是否包含“.org”。

 

使用Lambdas进行变量捕获

尽管这些lambd的简单用法很不错,但是可变捕获是真正使lambda函数发挥作用的秘诀。假设您要创建一个小的函数来查找包含特定名称的地址。如果您可以编写类似这样的代码,那不是很好吗?

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

事实证明,此示例是完全合法的-并且显示了lambda的真正价值。我们可以将一个声明为在lambda(名称)之外的变量,并在lambda中使用它。当findMatchingAddresses调用我们的lambda函数时,其中的所有代码都将执行-并且在调用addr.find时,它可以访问用户传入的名称。我们要做的唯一要做的就是告诉编译器我们希望有变量捕获。我通过将[&]用作捕获规范,而不是[]。空[]告诉编译器不要捕获任何变量,而[&]规范则告诉编译器执行变量捕获。

那不是很棒吗?我们可以创建一个简单的函数以传递给find方法,捕获变量名,然后仅用几行代码就将其全部写入。为了在不使用C ++ 11的情况下获得类似的行为,我们要么需要创建整个函子类,要么需要在AddressBook上使用专门的方法。在C ++ 11中,我们可以有一个简单的AddressBook接口,可以非常轻松地支持任何类型的过滤。

只是为了好玩,假设我们只想查找长度超过一定字符数的电子邮件地址。同样,我们可以轻松做到这一点:

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

顺便说一句,要从Herb Sutter窃取线路,您应该习惯于看到“});” 这是标准的函数接受lambda语法,并且您在自己的代码中开始看到和使用lambda的次数越多,您所看到的语法就越少。

Lambda和STL

毫无疑问,lambda函数的最大受益者之一是标准模板库算法包的高级用户。以前,使用for_each之类的算法是一种曲解。现在,尽管如此,您几乎可以像编写普通循环一样使用for_each和其他STL算法。相比:

vector <int> v;
v.push_back(1);
v.push_back(2);
// ...
对于(auto itr = v.begin(),end = v.end(); itr!= end; itr ++)
{
    cout << * itr;
}

vector <int> v;
v.push_back(1);
v.push_back(2);
// ...
for_each(v.begin(),v.end(),[](int val)
{
    cout << val;
});

如果您问我,代码看起来很漂亮-它像正常循环一样读取并结构化,但是您突然可以利用for_each提供的好处而不是常规的for循环-例如,保证您拥有正确的结束条件。现在,您可能想知道,这不会杀死性能吗?好吧,这是一个关键:事实证明for_each具有大致相同的性能,有时甚至比常规的for循环还要。(原因:它可以利用循环展开的优势。)

如果您对C ++ 11 lambda以及STL的好处感兴趣,您将喜欢这段关于Herb Sutter 谈论C ++ 11 lambda的视频。

我希望这个STL示例向您展示lambda函数不仅仅是创建函数的一种更便捷的方式-它们允许您创建全新的编写程序的方式,其中您具有将其他函数作为数据的代码,并允许您抽象出特定数据结构的处理。for_each工作在一个列表上,但是拥有与树配合使用的类似功能不是很好,在这里您要做的只是处理一些可处理每个节点的代码,而不必担心遍历算法?这种分解使一个函数担心数据的结构,而将数据处理委托给另一个函数则可能非常强大。借助lambda,C ++ 11启用了这种新型编程。不是说您以前不可能做过-for_each不是新的-它

有关新Lambda语法的更多信息

顺便说一句,如果要使用零参数的函数,则参数列表(如返回值)也是可选的。也许最短的lambda表达式是:

[] {}

这是一个不带参数也不执行任何操作的函数。仅有的一个更引人注目的示例:

using namespace std;
#include <iostream>

int main()
{
    [] { cout << "Hello, my Greek friends"; }();
}

就我个人而言,我并没有因为忽略论点清单而被卖掉。我认为[]()结构倾向于帮助lambda函数在代码中更加突出,但是时间会证明人们提出了什么标准。

返回值

默认情况下,如果您的lambda没有return语句,则默认为void。如果您有一个简单的返回表达式,则编译器将推断出返回值的类型:

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

如果编写的返回值多于一个更复杂的lambda函数,则应指定返回类型。(即使您有多个return语句,某些编译器(例如GCC)也会让您不做任何事情而逃脱,但是标准并不能保证它。)

Lambda使用可选的新C ++ 11返回值语法,该语法将返回值放在函数之后。实际上,如果要指定lambda的返回类型,则必须执行此操作。这是上面真正简单示例的更明确的版本:

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

Throw Specifications

尽管C ++标准委员会决定弃用抛出规范(除了我将在以后的文章中介绍的几种情况,但他们并未从语言中删除它们),并且有一些工具可以进行静态代码分析以检查异常 规范,例如PC Lint。如果您正在使用这些工具之一进行编译时异常检查,那么您确实希望能够说出您的lambda函数抛出哪些异常。我看到的主要原因是当您将lambda函数作为参数传递给另一个函数时,该函数期望lambda函数仅引发一组特定的异常。通过为lambda函数提供异常规范,您可以允许PC Lint之类的工具为您检查该异常。如果您想这样做,事实证明您可以。这是一个lambda,它指定不接受任何参数且不引发异常:

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

Lambda封包如何实施?

How are Lambda Closures Implemented?

那么,变量捕获的魔力真的如何发挥作用?事实证明,lambda的实现方式是创建一个小类。此类重载了operator(),因此它的作用就像一个函数。Lambda函数是此类的实例;构造该类时,周围环境中的任何变量都将传递到lambda函数类的构造函数中,并保存为成员变量。实际上,这有点像 已经有可能的仿函数的想法。C ++ 11的好处在于,这样做几乎变得非常容易-因此您可以一直使用它,而不仅仅是在非常罕见的情况下编写一个全新的类才有意义。

C ++对性能非常敏感,实际上,它为您捕获了哪些变量以及如何捕获这些变量提供了很大的灵活性,所有这些都通过捕获规范[]来控制。您已经看到了两种情况-没有任何内容,没有捕获任何变量,有&时,通过引用捕获了变量。如果使用空的捕获组[]来创建lambda,而不是创建类,则C ++将创建常规函数。以下是选项的完整列表:

[]一无所获(或者焦土策略?)
[&]通过引用捕获任何引用的变量
[=]通过复制捕获任何引用的变量
[=,&foo]通过复制来捕获任何引用的变量,但通过引用捕获变量foo
[酒吧]通过复制来获取栏;不要复制其他任何东西
[这个]捕获封闭类的this指针

请注意最后一个捕获选项-如果您已经指定了默认捕获(=或&),则无需包含它,但是可以捕获函数的this指针这一事实非常重要,因为这意味着编写lambda函数时,无需在局部变量和类的字段之间进行区分。您可以同时访问两者。很棒的事情是您不需要显式使用this指针;就像您正在内联编写函数一样。

class Foo
{
public:
    Foo () : _x( 3 ) {}
    void func ()
    {
        // a very silly, but illustrative way of printing out the value of _x
        [this] () { cout << _x; } ();
    }

private:
        int _x;
};

int main()
{
    Foo f;
    f.func();
}

通过引用捕获的危害和好处

当您通过引用捕获时,lambda函数能够修改lambda函数之外的局部变量-毕竟是引用。但这也意味着,如果从函数返回lamba函数,则不应使用按引用捕获,因为在函数返回后,该引用将无效。

Lambda是什么类型?

您要创建lambda函数的主要原因是有人创建了一个预期会接收lambda函数的函数。我们已经看到,我们可以使用模板将lambda函数作为参数,并自动保留lambda函数作为局部变量。但是,您如何命名特定的lambda?因为每个lambda函数都是通过创建一个单独的类来实现的,就像您前面所看到的那样,即使单个lambda函数实际上也是一个不同的类型,即使这两个函数具有相同的参数和相同的返回值!但是C ++ 11确实包含一个方便的包装器,用于存储任何类型的函数-lambda函数,函子或函数指针:std :: function。

std ::功能

新的std :: function是一种将lambda函数作为参数和返回值传递的好方法。它允许您为参数列表和模板中的返回值指定确切的类型。这是AddressBook示例,这次使用std :: function而不是模板。注意,我们确实需要使用“ functional”头文件。

#include <functional>
#include <vector>

class AddressBook
{
    public:
    std::vector<string> findMatchingAddresses (std::function<bool (const string&)> func)
    { 
        std::vector<string> results;
        for ( auto itr = _addresses.begin(), end = _addresses.end(); itr != end; ++itr )
        {
            // call the function passed into findMatchingAddresses and see if it matches
            if ( func( *itr ) )
            {
                results.push_back( *itr );
            }
        }
        return results;
    }

    private:
    std::vector<string> _addresses;
};

与模板相比,std :: function的一大优势在于,如果编写模板,则需要将整个函数放在头文件中,而std :: function则不需要。如果您要处理的代码会发生很大变化并且包含在许多源文件中,那么这确实可以提供帮助。

如果要检查std :: function类型的变量当前是否持有有效函数,可以始终将其视为布尔值:

 

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

关于函数指针的注意事项

在最终的C ++ 11规范下,如果您的lambda带有空的捕获规范,则可以将其视为常规函数并分配给函数指针。这是将函数指针与无捕获lambda一起使用的示例:

翻译过来:

A Note About Function Pointers

Under the final C++11 spec, if you have a lambda with an empty capture specification, then it can be treated like a regular function and assigned to a function pointer. Here's an example of using a function pointer with a capture-less lambda:

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

之所以可以这样做是因为没有捕获组的lambda不需要自己的类-可以将其编译为常规的旧函数,从而使其像普通函数一样被传递。不幸的是,MSVC 10中不包含对该功能的支持,因为将其添加到标准中为时已晚。

用Lambdas派代表

让我们再看一个lambda函数的示例-这次创建一个委托。你问什么是代理人?当您调用普通函数时,您只需要函数本身即可。在对象上调用方法时,需要两件事:函数和对象本身。这是func()和obj.method()之间的区别。要调用方法,您需要同时使用。仅将方法的地址传递给函数是不够的。您需要有一个对象才能调用该方法。

让我们看一个示例,从一些代码开始,该代码再次将函数作为参数,然后将一个委托传递给该函数。

#include <functional>
#include <string>

class EmailProcessor
{
public:
    void receiveMessage (const std::string& message)
    {
        if ( _handler_func ) 
        {
            _handler_func( message );
        }
        // other processing
    }
    void setHandlerFunc (std::function<void (const std::string&)> handler_func)
    {
        _handler_func = handler_func;
    }

private:
        std::function<void (const std::string&)> _handler_func;
};

这是一种非常标准的模式,允许在发生有趣的事情时向类注册回调函数。

但是,现在让我们说我们想要另一个负责跟踪到目前为止收到的最长消息的类(为什么要这样做?也许您是个无聊的sysadmin)。无论如何,我们可以为此创建一个小类:

#include <string>

class MessageSizeStore
{
    public:
    MessageSizeStore () : _max_size( 0 ) {}
    void checkMessage (const std::string& message ) 
    {
        const int size = message.length();
        if ( size > _max_size )
        {
            _max_size = size;
        }
    }
    int getSize ()
    {
        return _max_size;
    }

private:
    int _max_size;
};


如果我们想在消息到达时调用checkMessage方法怎么办?我们不能只传递checkMessage本身-它是一种方法,因此它需要一个对象。

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

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

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

那不是很酷吗?我们在这里使用lambda函数作为粘合代码,从而使我们可以将常规函数传递给setHandlerFunc,同时仍然可以调用方法-使用C ++创建一个简单的委托。

综上所述

那么,如果lambda函数在没有它们的情况下生存了数十年,那么lambda函数真的会开始在C ++代码中各处出现吗?我想是的-我已经开始在生产代码中使用lambda函数,并且它们开始在所有地方出现-在某些情况下,缩短了代码,在某些情况下改进了单元测试,在某些情况下替换了以前可以使用的功能仅使用宏即可完成。是的,我认为lambda的作用远胜于其他任何希腊字母。

Lambda函数在GCC 4.5及更高版本以及MSVC 10和Intel编译器的版本11中可用。

翻译:

In Summary

So are lambda functions really going to start showing up all over the place in C++ code when the language has survived for decades without them? I think so--I've started using lambda functions in production code, and they are starting to show up all over the place--in some cases shortening code, in some cases improving unit tests, and in some cases replacing what could previously have only been accomplished with macros. So yeah, I think lambdas rock way more than any other Greek letter.

Lambda functions are available in GCC 4.5 and later, as well as MSVC 10 and version 11 of the Intel compiler.

 

本文参考:https://www.cprogramming.com/c++11/c++11-lambda-closures.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值