lambda表达式

一、定义

一个lambda表达式表示一个可调用的代码单元,可以将其理解为一个未命名的内联函数。与任何函数类似,一个lambda具有一个返回类型、一个参数列表和一个函数体。但与函数不同,lambda可能定义在函数内部。
lambda表达式的具体形式如下
[capture list](parameter list)->return type {function body}
其中,capture list是一个lambda所在函数中定义的局部变量的列表(通常为空);return type、parameter list和function body与任何普通函数一样,分别表示返回类型、参数列表和函数体。但与普通函数不同,lambda必须使用尾置返回来指定返回类型。一般可以忽略参数列表(忽略括号和参数列表等价于指定一个空参数列表)和返回类型(如果函数体只有一个return语句,则返回类型从返回的表达式的类型推断而来,否则返回类型被认为是void。如果不是必须使用尾置返回类型),但必须永远包含捕获列表和函数体,例如:

auto func = []{return 42;};
cout << func() << endl;

lambda表达式不能有默认参数。因此,一个lambda调用的实参数目永远与形参数目相等。
空捕获列表表示此lambda不使用它所在函数中的任何局部变量。
虽然一个lambda可以出现在一个函数中,使用其局部变量,但它只能使用那些明确指明的变量。lambda表达式只能捕获局部非static变量,但可以直接使用局部static变量和它所在函数之外声明的名字。lambda表达式也不能捕获全局变量,会引发错误。
当lambda表达式函数体中不知有一个return语句时,必须指定返回类型。例如:

// 错误方式
transform(vi.begin(), vi.end(), vi.begin(),
    [](int i){if (i < 0) return -i;
        else return i;});
// 正确方式
transform(vi.begin(), vi.end(), vi.begin(),
    [](int i)->int {if (i < 0) return -i;
        else return i;});

lambda表达式是函数对象(如果一个类定义了调用运算符operator(),则该类的对象称为函数对象。因为该对象的行为就像函数一样)。其产生的类只有一个函数调用运算符成员,它的形参列表和函数体与lambda表达式完全一样。例如:

auto cmp = [](const string &a, const string &b){
    return a.size() < b.size();};
// 其等价于
class ShorterString {
public:
    bool operator()(const string &s1, const string &s2) const {
        return s1.size() < s2.size();
    }
};

lambda表达式产生的类不含默认的构造函数、赋值运算符及默认的析构函数;它是否含有默认的拷贝/移动构造函数则通常要视捕获的数据成员类型而定。

使用lambda表达式有4个原因
(1)lambda表达式的定义和使用在同一地方进行,比函数更便于在大程序中进行修改和复制粘贴;
(2)lambda表达式的代码比函数更为简洁
(3)如果使用函数指针,编译器会阻止将其内联,但编译器不会阻止对lambda表达式进行内联,其使用效率比函数指针更高
(4)lambda表达式还有一些其他功能,其可访问作用域内的任何动态变量,可通过[&]按引用访问,[=]按值访问,以及混合访问。

lambda表达式对应的旧概念是函数指针,虽然lambda表达式的大部分功能都是函数指针所无法实现的。
lambda表达式提供了一种使用匿名函数的服务,对使用函数谓词的STL算法尤其如此。
在C++11中,对于接受函数指针或函数符的函数,可使用匿名函数定义(lambda)作为其参数,这是lambda表达式的主要用途
lambda表达式除了能在STL算法中作为谓词使用外,还能用来创建自定义构造器,以及对谓词做特化处理来提供条件变量给线程API。
对于只在一两个地方使用的简单操作,lambda表达式是最有用的。如果需要在很多地方使用相同的操作,通常应该定义一个函数,而不是多次编写相同的lambda表达式。类似的,如果一个操作需要很多语句才能完成,通常使用普通函数较好。

二、参数捕获

闭包是lambda表达式创建的运行期对象,根据不同的捕获模式,闭包会持有数据的副本或引用。例如:

std::find_if(container.begin(), container.end(), [](int val){return 0<val && val<10;});

其中闭包就是作为第3个实参在运行期间传递给find_if的对象。
闭包类是实例化闭包的类。每个lambda表达式都会触发编译器生成一个独一无二的闭包类。而闭包中的语句变成它的闭包类成员函数的可执行指令。
lambda表达式常用于创建闭包并仅将其用作传递给函数的实参。
lambda表达式可以通过赋值操作符来创建副本。
C++11中有2种默认的捕获模式:按引用或按值。按引用的默认捕获模式可能导致空悬引用,按值的默认捕获模式会忽悠你,好像可以对空悬引用免疫(其实并没有),还让你认为你的闭包是独立的(事实上它们可能不是独立的)。(尤其是在类的成员函数中使用lambda表达式时)。从长远来看,显式列出lambda表达式所依赖的局部变量或形参是好的工程实践
尽量保持lambda 的变量捕获简单化。一个lambda捕获从lambda被创建(即定义lambda的代码被执行时)到lambda自身执行(可能多次执行)这段时间内保存的相关信息。确保lambda每次执行的时候这些信息都有预期的意义,这是程序员的责任。
如果捕获一个int,string等非指针类型的普通变量,通常可采用值捕获方式。如果捕获一个指针或迭代器,则应使用引用捕获
一般来说,我们应该尽量减少捕获的数据量,来避免潜在的捕获导致的问题。而且,如果可能的话,应该避免捕获指针或引用
类似参数传递,变量的捕获方式也可以是值或引用。采用值捕获的前提是变量可以复制。与参数不同,被捕获的变量的值是在lambda创建时复制的,而不是调用时复制。例如:

void func() {
    size_t v1 = 42; // 局部变量
    auto f = [v1]{return v1;};
    v1 = 0;
    auto j = f(); // j=42, f保存了创建它时v1的副本
}

由于被捕获变量的值是在lambda创建时的副本,因此随后对其修改不会影响到lambda内对应的值。
引用捕获,例如:

void func2() {
    size_t v1 = 42; // 局部变量
    auto f = [v1]{return v1;};
    v1 = 0;
    auto j = f(); // j=0, f保存了v1的引用 当lambda返回v1时, 其返回的是v1指向的对象的值
}

采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行时是存在的。lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,此时捕获的引用指向的局部变量已经消失,会引发错误。
除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据lambda函数体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个&或=,&告诉编译器所有变量采用引用捕获,=告诉编译器所有变量采用值捕获。例如:

wc = find_if(words.begin(), words.end(), 
    [=](const string &s){return s.size()>=sz;});

如果希望对一部分变量采用值捕获,对其他变量采用引用捕获,则可以混合使用隐式捕获和显式捕获。当混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个&或=。此符号指定了默认捕获方式为引用或值捕获。例如:

void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os=cout, char c=' ') {
    // os隐式捕获 采用引用捕获 c显式捕获 值捕获
    for_each(word.begin(), word.end(),
        [&, c](const string &s){os << s << c;});
    // os显式捕获 引用捕获 c隐式捕获 值捕获
    for_each(word.begin(), word.end(),
        [=, &os](const string &s){os << s << c;});
}

在lambda表达式的捕获列表中,还可以出现this,这出现在当你的lambda表达式在一个类内,这时可以在lambda的函数体内使用类的成员且可以省略this指针
其还允许出现这样的代码:[const auto &x = f(y)],其表示把f(y)返回的引用复制进去,并起个名字叫x。但这种方式很不常见。
当使用auto定义一个用lambda初始化的变量时,定义了一个从lambda生成的类型的对象。类似任何普通类的数据成员,lambda的数据成员也在lambda对象创建时被初始化。

如果希望能改变一个被值捕获变量的值,就必须在参数列表首加上关键字mutable。例如:

void func() {
    size_t v1 = 42;
    auto f = [v1]mutable{return ++v1;};
    v1 = 0;
    auto j = f(); // 此时j为43
}

而一个引用捕获的变量是否可以修改,依赖于此引用指向的是一个const类型还是非const类型,例如:

void func() {
    size_t v1 = 42;
    // v1是非const变量 可以通过f2进行修改
    auto f2 = [&v1]{return ++v1;};
    v1 = 0;
    auto j = f2(); // 此时j为1
}

我们可以从一个函数返回lambda。函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个lambda,则与函数不能返回一个局部变量的引用类似,此lambda也不能包含引用捕获。

有时,按值捕获和按引用捕获皆非你所愿。如果想把一个只移对象(例如unique_ptr)放入闭包,C++14提供了为对象移动入闭包的直接支持。而C++11中有近似达成移动捕获的方法
STL提供了称为初始化捕获(也称为广义lambda捕获)的机制,初始化捕获不能表示的,就是默认的捕获模式(其不应该被使用)。C++11捕获可以实现的情况下,使用初始化捕获语法会显得啰嗦,所以若能用C++11捕获,则直接使用
使用初始化捕获,则有机会指定:
(1)由lambda表达式生成的闭包类中的成员变量的名字;
(2)一个表达式,用以初始化该成员变量。
在C++11中模拟初始化捕获的方法为
(1)把需要捕获的对象移动到std::bind产生的函数对象中;
(2)给lambda表达式一个指涉到欲捕获对象的引用。
例如:

// C++14中
std::vector<double> data; // 欲移入闭包的对象
auto func = [data=std::move(data)](){// 对数据的操作}; // C++14的初始化捕获
// C++11中
std::vector<double> data;
auto func = std::bind([](const std::vector<double> &data){// 对数据的操作}, std::move(data)); // 模拟初始化捕获

又例如:

// C++14中
auto func = [pw=std::make_unique<Widget>()](){return pw->isValidated() && pw->isArchived();}; // 初始化捕获
// C++11中
auto func = std::bind([](const std::unique_ptr<Widget> &pw){return pw->isValidated() && pw->isArchived();}, std::make_unique<Widget>());

默认情况下,lambda表达式生成的闭包类中的operator()成员函数会带有const饰词,于是闭包里的所有成员变量在lambda表达式的函数体内都会带有const饰词。但绑定对象里移动构造的数据副本却不带const饰词。所以为了防止该数据的副本在lambda表达式内被意外修改,lambda表达式的形参被声明为常量引用。但如果lambda表达式声明中带有mutable饰词,则闭包的operator()函数就不会在声明时带有const饰词。

三、lambda与std::bind

std::bind()函数定义在functional头文件中,可以看作一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。调用std::bind的一般形式为:

auto newCallable = std::bind(callable, arg_list);

其中,newCallable是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。即当我们调用newCallable时,newCallable会调用callable,并传递arg_list作为其参数。arg_list中的参数可能包含形如_n的名字,其中n是一个整数。这些参数是占位符,表示newCallable的参数,它们占据传递给newCallable的参数位置,即在callable中该位置的参数不给出,留待后续调用时再指定。例如:

using namespace std::placeholders;
bool check_size(const string &s, string::size_type sz) {
    return s.size() >= sz;
}
auto check6 = std::bind(check_size, _1, 6);
string s = "hello";
bool b1 = check6(s); // 相当于调用check_size(s, 6)

在C++11中,相比于std::bind,lambda表达式是更好的选择,而在C++14中,则不推荐再使用std::bind
之所以优先选用lambda表达式,主要是因为lambda表达式具备更高的可读性,且更易于理解和维护,效率也更高。例如:

// lambda表达式方法
auto setSoundL = [](Sound s){
    using namespace std::chrono;
    setAlarm(steady_clock::now() + hours(1), s, seconds(30));
};
// std::bind表示方法
using namespace std::chrono;
using namespace std::placeholders;
auto setSoundB = std::bind(
    setAlarm, std::bind(std::plus<steady_clock::time_point>(),
    steady_clock::now(), hours(1)), _1, seconds(30));

在上述C++11模拟初始化捕获的情况下,是使用std::bind的2种合适情况之一
另一种适合使用std::bind的情况是:在C++11中处理多态函数对象时
因为绑定对象的函数调用运算符利用了完美转发,它就可以接受任何型别的实参。这个特点在你想要绑定的对象具有一个函数调用运算符模板时,是有利用价值的。例如:

// 先给定一个类
class PolyWidget {
public:
    template<typename T>
    void operator()(const T& param);
    ...
};
// 进行绑定
PolyWidget pw;
auto boundPW = std::bind(pw, _1);
// 这样 boundPW就可以通过任意型别的实参加以调用
boundPW(1930); // 传递int给PolyWidget::operator()
boundPW(nullptr); // 传递nullptr给PolyWidget::operator()
boundPW("Rosebud"); // 传递字符串给PolyWidget::operator()

在C++11中使用lambda表达式是无法实现上述效果的。

四、lambda与std::function

泛型lambda表达式是C++14的重要新特性,此时,lambda可以在形参中使用auto。其正确使用时,应对auto&&型别的形参使用decltype,并加以std::forward。例如:

auto f = [](auto &&param){return func(normalize(std::forward<decltype(param)>(param)));};

C++11中不具备该特性,必须指明形参类型。

lambda表达式的类型是没有名字的,通常使用模板来代替。例如:

template<typename T>
int Count(vector<int> &number, T filter) {
    int counter = 0;
    for (int x: number) {
        if (filter(x)) {
            counter++;
        }
    }
    return counter;
}
int CountOdds(vector<int> &number) {
    return Count(number, [](int x){return x % 2 == 1;});
}
int CountEvens(vector<int> &number) {
    return Count(number, [](int x){return x % 2 == 0;});
}

当需要返回一个函数时,都使用lambda表达式来做,但lambda表达式的类型是没有名字的,因此其返回值只能是std::function。例如:

std::function<int(int)> Adder(int x) {
    return [=](int y){return x + y;};
}

一般来说都建议使用模板参数代表函数,从而可以使用lambda表达式。而std::function都是在必须把lambda表达式保存下来的时候使用。常见情况有
(1)要使用vector来保存几个lambda表达式,vector的元素只能是相同的类型,而每个lambda表达式都有独有的类型,因此需要使用std::function。
(2)我们经常使用h文件和cpp文件来隔离接口和实现,这时使用lambda表达式却看不到lambda的定义,此时需要使用std::function,不然lambda表达式的代码就必须出现在头文件中,这在很多时候是没必要的。
使用std::function来代替模板会造成性能损失,在大部分情况下都是值得的。可以把程序用模板和std::function分别写一次,然后使用性能检测工具来进行测试比较。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值