C++(标准库):33---Lambda表达式

 一、lambda表达式概述

  • lambda自C++11引入。这带来一种很具为例又十分便利的方法,允许我们提供局部机能,特别是用来具体指明算法和成员函数的细节lambda为C++带来十分重要而深具意义的改善(当我们使用STL时)。因为如今你有了一个直观、易读的方式,可以将独特的行为传递给算法和容器的成员函数
  • 一个lambda表达式可表示一个可调用的代码单元,可以将其理解为一个未命名的内联函数

二、格式

  • 与任何函数相同:一个lambda包含:返回类型、参数列表、函数体
  • 与普通函数不同:lambda使用尾指返回来指定返回类型

  • capture list(捕获列表):是一个lambda所在函数中定义的局部变量的列表(通常为空)
  • return type:返回类型。如果忽略返回类型,lambda根据函数体的代码自动推断出返回类型
  • parameter list:参数列表
  • function body:函数体。如果是一条定义语句花括号后面要有分号;。如果是调用则分号可省去

例如

  • lambda的参数列表/捕获列表为空,忽略了返回类型
auto f = [] {return 100; };
cout << f()<< endl; //100

三、向lambda传递参数

  • lambda的实参与形参类型必须匹配,并且不能有默认参数
auto f = [](int a, int b)->int {return a + b; };
cout << f(1,2)<< endl;
//实现sort的从大到小排序算法
auto f = [](const int& a, const int& b) {return a > b; };
vector vec{ 1,2,10,1545,151 };

sort(vec.begin(), vec.end(),
		[](const int& a, const int& b) {return a > b; });

sort(vec.begin(), vec.end(),f);

四、lambda捕获

  • lambda的函数体中只能使用捕获列表指定的参数,如果捕获列表没有则不能使用
int sz=10;
int a;

//lambda捕获了sz,但是没有捕获a
[sz](const string &a) {
    //a=10; //错误,不能调用a
    return a.size() >= sz;//正确
};

C++(标准库):33---Lambda表达式_第1张图片

  • 捕获分为:
    • 值捕获
    • 引用捕获
    • 隐式捕获
    • 混合隐式捕获

值捕获

  • 值捕获的传参方式是拷贝传参
  • 值的拷贝是在lambda表达式创建时的拷贝,而不是调用时的拷贝
  • 因为是值拷贝,所以当捕获的值在外部改变时,该值在lambda内不会跟随变化
  • 重点:使用值捕获时,该值在lambda内不能被改变(下面会介绍使用mutable关键字改变)
size_t v1=42;

auto f=[v1]() {return v1;};
//auto f=[v1]() {return ++v1;}; //错误,值捕获不能改变值

v1=0;
auto j=f(); //j=42

引用捕获

  • 引用捕获的值是以引用的方式使用该值
  • 所以当值在外部发生变化时,该值lambda中也会改变
  • 引用捕获可以在lambda内改变该值的值
  • 因为采用引用方式捕获一个变量,所以在调用lambda表达式时,必须保证这个值是存在的(例如局部变量在函数执行完之后消失。例如函数返回一个lambda表达式,而该表达式引用函数内的一个局部变量)
size_t v1=42;
auto f=[&v1]() {return v1;};
v1=0;
auto j=f(); //j=0

隐式捕获

  • 可以在lambda的[]号中输入符号,让编译器自己捕获的类型。并且可以省去参数
  • 如果在捕获列表写一个&:告诉编译器该lambda使用引用捕获。如果在捕获列表写一个=:告诉编译器该lambda使用值捕获
int sz = 2;
vector vec{ "A","ABC","ABCD" };

//sz为隐式捕获:值捕获方式
auto wc = find_if(vec.begin(), vec.end(),
	[=](const string &s) {return s.size() >= sz;  });

混合隐式捕获方式

  • 使用混合隐式捕获时,第一个元素必须是一个&或者=
  • 显示捕获的变量必须使用与隐式捕获不同的方式:如果隐式捕获是引用方式(使用了&),则显式捕获命名变量必须采用值方式,因为不能在其名字前使用&。
  • 类似的,如果隐式捕获采用的是值捕获(使用了=),则显式捕获命名变量必须采用引用方式(在名字前使用&)
void biggers(vector &words, vector::size_type sz,
    ostream &os = cout, char c = ' ')
{
    //os隐式捕获:引用捕获;c显式捕获:值捕获方式
    for_each(words.begin(), words.end(),
        [&, c](const string &s) {os << s << c; });
    //os显式捕获:引用捕获;c隐式捕获:值捕获方式
    for_each(words.begin(), words.end(),
        [=, &os](const string &s) {os << s << c; });
}

C++(标准库):33---Lambda表达式_第2张图片

五、可变lambda(mutable关键字)

  • 默认情况下:lambda值捕获时,该值在lambda内不能被改变
  • 使用mutable关键字之后:该值可以在lambda表达式内被改变。但是lambda内的值仍与外界值的改变无关
int val=1;
auto f = [val]() mutable {return ++val; };

val = 0; //将val变为0
auto j = f(); //j=2
cout << j << endl;

六、指定lambda的返回类型

  • 一般情况下,lambda会自动推断出表达式的返回值类型
  • 但是某些情况下需要显式的给出返回值类型,返回值类型使用尾指返回类型
//隐式的返回类型int
[](int i) {return i < 0 ? -i : i; }
//显式给出返回类型int
[](int i)->int
{
    if (i < 0) return -i;
    else return i;
};

七、lambda的类型

  • lambda的类型,是个不具名的function object(或functor)
  • 如果你想使用lambda的类型声明对象,可以借助于template std::function<>或auto
  • 如果你想写下该类型,则可以使用decltype()(见下面“十一”的演示案例②)

演示案例

#include 
#include 

std::function returnLambda()
{
    return [](int x, int y) {return x*y; };
}

int main()
{
    auto f = returnLambda();
    std::cout << f(3, 7); //21
}

八、lambda与binder

  • 例如我们在前面一篇文章中使用函数对象和函数适配器binder来书写代码,如下所示:
#include 
#include 
using namespace std::placeholders;
 
int main()
{
    auto plus10 = std::bind(std::plus(), _1, 10);
    //相等于调用std::plus(7,10)
    std::cout << "7+10=" << plus10(7) << std::endl;
 
    auto plus10times2 = std::bind(std::multiplies(), 
                                    std::bind(std::plus(), _1, 10), 
                                    2);
    //相等于调用std::multiplies(std::plus(10,10),2)
    std::cout << "(10+10)*2=" << plus10times2(10) << std::endl;
 
    //相等于调用std::multiplies(std::multiplies(7,7),7)
    auto pow3 = std::bind(std::multiplies(), 
                            std::bind(std::multiplies(), _1, _1),
                            _1);
    std::cout << "7*7*7=" << pow3(7) << std::endl;
 
    //相等于调用std::divides(7,49)
    auto inversDivide = std::bind(std::divides(), _2, _1);
    std::cout << "7/49=" << inversDivide(49, 7) << std::endl;
}

  • 对于上面的binder,代码比较复杂,如果使用lambda则可以如下所示:
//头文件同上
int main()
{
    auto plus10 = [](int i) {return i + 10; };
    std::cout << "7+10=" << plus10(7) << std::endl;

    auto plus10times2 = [](int i) {return (i + 10) * 2; };
    std::cout << "(10+10)*2=" << plus10times2(10) << std::endl;

    auto pow3 = [](int i) {return i*i*i; };
    std::cout << "7*7*7=" << pow3(7) << std::endl;

    auto inversDivide = [](double d1, double d2) {return d2 / d1; };
    std::cout << "7/49=" << inversDivide(49, 7) << std::endl;
}

C++(标准库):33---Lambda表达式_第3张图片

九、lambda与带有状态的函数对象

演示案例①

  • lambda是不带状态的
  • 例如下面我们修改C++(标准库):31文章中的演示案例:
#include 
#include 
#include 
using namespace std;

int main()
{
    vector coll{ 1,2,3,4,5,6,7,8 };

    long sum = 0;
    for_each(coll.begin(), coll.end(), [&sum](int elem) {sum += elem; });

    double mv = static_cast(sum) / static_cast(coll.size());
}

演示案例②

  • 例如我们修改C++(标准库):31文章中“七”的演示案例。如下:
#include 
#include 
#include 
using namespace std;

int main()
{
	std::list coll{ 1,2,3,4,5,6,7,8,9,10 };
	for (const auto elem : coll)
		std::cout << elem << " ";
	std::cout << std::endl;

	std::list::iterator pos;
	int count = 0;
	pos=remove_if(coll.begin(),coll.end(),
		[count](int)mutable {return ++count == 3; }
	);
	coll.erase(pos, coll.end());
	for (const auto elem : coll)
		std::cout << elem << " ";
}
  • 上面运行的代码与那篇文章演示案例运行的结果一样,第3和第6个元素都会被移除:
    • lambda使用了mutable,remove_if()算法在执行过程中复制了一份,于是存在两个lambda对象都移除第三元素,导致重复行为

  • 如果你已by reference方式传递实参,那么结果就正确了,只移除第三个元素:
pos=remove_if(coll.begin(),coll.end(),
    [&count](int) {return ++count == 3; }
);

十、lambda调用全局函数和成员函数

演示案例(调用全局函数)

  • 下面的演示案例是《C++标准库》p490页演示案例的lambda版本。代码如下:
#include 
#include 
#include 
#include 
using namespace std;

char myTopper(char c)
{
    std::locale loc;
    return std::use_facet>(loc).toupper(c);
}

int main()
{
    string s("Internationlization");
    string sub("Nation");

    string::iterator pos;
    pos=search(s.begin(),s.end(),sub.begin(),sub.end(),
        [](char c1, char c2) {return myTopper(c1) == myTopper(c2); }
    );
    if (pos != s.end())
        std::cout << "\"" << sub << "\" is part of \"" << s << "\"" << std::endl;
}

演示案例(调用成员函数)

  • 下面的演示案例是前面一篇文章(https://blog.csdn.net/qq_41453285/article/details/105486301)演示案例的lambda版本。代码如下:
#include 
#include 
#include 
#include 
using namespace std;

class Person
{
private:
	std::string name;
public:
	Person(const std::string& _name) :name(_name) {}
	void print()const { std::cout << name << std::endl; }
	void print2(const std::string& prefix)const { std::cout << prefix << name << std::endl; }
};

int main()
{
	std::vector vec{ Person("Tick"),Person("Trick"),Person("Track") };

	for_each(vec.begin(), vec.end(), [](const Person& p) {p.print(); });
	std::cout << std::endl;

	for_each(vec.begin(), vec.end(), [](const Person& p) {p.print2("Person:"); });
	std::cout << std::endl;

	return 0;
}

十一、lambda作为Hash函数、排序准则或相等准则

演示案例①

  • 例如,下面我们使用deque<>存储自定义的Person对象,然后调用sort()算法对deque<>内的Person对象进行排序,其中排序的时候使用lambda设计排序准则
#include 
#include 
#include 
using namespace std;

class Person
{
public:
    std::string firstname()const { return _firstName; }
    std::string lastname()const { return _lastName; }
private:
    std::string _firstName;
    std::string _lastName;
};

int main()
{
    std::deque coll;
	
    sort(coll.begin(), coll.end(), 
        [](const Person& p1, const Person& p2) {return p1.firstname() < p2.firstname() || (p1.firstname() == p2.firstname() && p1.lastname() < p2.lastname()); }
    );
}

演示案例②

  • 例如,下面使用lambda指定unordered_set<>的hash函数和等价准则
#include 
#include 
using namespace std;

class Customer
{
public:
    Customer(const std::string& fn, const std::string& ln, long n) :fname(fn), lname(ln), no(n) {}
    std::string firstname()const { return fname; }
    std::string lastname()const { return lname; }
    long number()const { return no; }
private:
    std::string fname;
    std::string lname;
    long no;
};

int main()
{
    auto hash = [](const Customer& c) {
        //这个hash_val()是自定义函数,可以参阅《C++标准库》的hashval.hpp
        return hash_val(c.firstname(), c.lastname());
    };

    auto eq = [](const Customer& c1, const Customer& c2) {
        return c1.number() == c2.number();
    };

    unordered_set custset(10, hash, eq);
    custset.insert(Customer("nico", "josuttis", 42));
}
  • 因为hash和eq都是lambda,因此需要使用decltype()产生lambda的类型,然后再传递给容器
  • 此外你也必须传递一个hash函数和相等准则给构造函数,构造构造函数会调用hash函数和相等准则的默认构造函数,而那对lambda而言是未定义的

十二、lambda的局限

  • 局限1:例如,我们想使用lambda作为关联式容器set的排序准则,但是不能直接将lambda表达式声明为set<>的模板参数2,而是要使用decltype()声明lambda的类型 
class Person{};

//使用set的默认排序规则存储Person对象
std::set coll;

//使用自定义的lambda准则排序set中的Person对象
auto cmp = [](const Person& p1, const Person& p2) {return p1.firstname() < p2.firstname() || (p1.firstname() == p2.firstname() && p1.lastname() < p2.lastname()); };
std::set coll;
  • 局限2:另外,你也必须把lambda对象传给coll的构造函数,构造coll会调用被传入的排序准则的默认构造函数,而lambda没有默认构造函数,也没有赋值运算符
  • 基于这些局限,“以class定义某个函数对象作为排序准则”说不定该比较直观些 
  • 局限3:它们无法拥有“跨越多次调用”都能被保存下来的内部状态,如果你需要这样的状态,必须在外围作用域中声明一个对象或变量,将其以by-reference方式传入lambda
  • 与此相比,函数对象允许你封装内部状态(见前面几篇“函数对象”文章的介绍)
  • 尽管如此,你还是可以使用lambda为无序容器指出一个hash函数或一个等效准则(见上面“十一”中的演示案例②)

你可能感兴趣的:(C++(标准库))