C++11 -- lambda表达式

概念

首先,Lambda表达式 属于C++原有语法在使用性上的增强。

  • 从本质上来讲,Lambda表达式 只是一种语法糖,因为所有其能完成的工作都可以使用C++其他稍微复杂的代码来实现。
  • 从广义上说,Lambda表达式 产生的是函数对象。在类中,可以 重载函数调用运算符() ,此时 类的对象 可以具有类似函数的行为,我们称这些对象为 对象函数(Function Object) 或者 仿函数(Functor)

Lambda表达式 实际上就是提供一个类似于匿名函数的特性。而匿名函数则是在需要一个函数时,但又不想费力去命名一个函数的情况下去使用的。利用 Lambda表达式 可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,使得代码更可读。

为什么要存在lambda表达式?

仿函数 在某些场景下使用起来不是很方便。如 :

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>

struct Goods
{
	std::string _name; // 名字
	double _price; // 价格
	int _evaluate; // 评价
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}

	friend std::ostream& operator<<(std::ostream& out, const Goods& good);

};

std::ostream& operator<<(std::ostream& out, const Goods& good)
{
	out << "name: " << good._name << ", price: " << good._price \
		<< ", evaluate: " << good._evaluate << std::endl;
	return out;
}


// 仿函数逻辑------------------------
struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};


struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};


int main()
{
	std::vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
	std::sort(v.begin(), v.end(), ComparePriceLess());

	for (auto& good : v)
	{
		std::cout << good << std::endl;
	}

	std::cout << "-------------------------------------------------" << std::endl;

	std::sort(v.begin(), v.end(), ComparePriceGreater());
	for (auto& good : v)
	{
		std::cout << good << std::endl;
	}

	return 0;
}

分析:

一个商品类 Goods 有多个属性,如果我们想对 多个商品对象 按照商品的某个属性进行排序(这里排序的工作使用算法库<algorithm>中的 std::sort),就需要传递一个函数对象,该函数对象提供了如何判断两个商品大小的逻辑,如果使用 仿函数,需要设计类并在内部重载()操作符。

当我们在阅读 main() 函数代码理解排序逻辑时,如果发现编写这段代码的开发人员命名不规范,如向 std::sort() 传递的仿函数对象名为 cmp1(),并且没有写相关注释,此时我们还需要找到该仿函数,查看该开发人员具体是如何重载()操作符的。这样就会浪费时间~


Lambda表达式 提供的匿名函数,就可以避免这个场景(这里先使用,后面会具体介绍~):

int main()
{
	std::vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
	// 升序
	std::sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price < g2._price;
	});

	for (auto& good : v)
	{
		std::cout << good << std::endl;
	}
	std::cout << "-------------------------------------------------" << std::endl;
	
	// 降序
	std::sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
		return g1._price > g2._price;
	});

	for (auto& good : v)
	{
		std::cout << good << std::endl;
	}
	return 0;
}

这样就不需要考虑命名问题,而且其他开发人员对这个排序逻辑一目了然。

Lambda 表达式的语法

Lambda表达式的语法结构如下:

[捕获列表](参数列表) mutable [例外规范] -> [返回类型] {函数体};
或者
[ capture-list ] (parameters) mutable(optional) constexpr(optional)(c++17) exception attribute -> ret { body }

看到这个语法格式,可能有些绝望,但最常用的格式是:

[捕获列表](参数列表) -> 返回类型 {函数体}

[ capture-list ] (parameters) -> ret { body };
[ capture-list ] (parameters){ body };
[ capture-list ] { body };

各个部分的解释

捕获列表 (capture list)

最前面的 [ ]Lambda表达式 一个很重要的功能,就是 闭包不能省略不写(详细介绍可查看闭包)。

闭包的一个强大之处在于 可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为 Lambda捕捉块 。具体如下():

  • [ ] 默认不捕获任何变量。
  • [=] 默认以捕获所有变量。
  • [&] 默认以引用捕获所有变量。
  • [x] 仅以捕获变量x,其他值不捕获。
  • [&x] 仅以引用捕获变量x,其他值不捕获。
  • [=, &x] 默认以捕获所有变量,但是变量 x 例外,通过引用捕获。
  • [&, x] 默认以引用捕获所有变量,但是变量 x 例外,通过捕获。
  • [this] 通过引用捕获当前对象。
  • [*this] 通过传值捕获当前对象。

捕获所有变量是指表达式所在作用域内

void test()
{
	int c = 3;
	auto f = [=] {
		return c;
	};
	std::cout << f() << std::endl;
}

int main()
{
	int a = 1, b = 2;
	test();
	return 0;
}

这里就不能捕获变量a 和 变量b

值捕获和引用捕获的区别

  • 对于捕获的变量,不可以在表达式函数体内进行更改。
  • 对于引用捕获的变量,可以在表达式函数体内进行更改。
int main()
{
	int a = 1;
	auto f1 = [=] {
		a++; // error
		std::cout << "a = " << a << std::endl; // right
	};

	auto f2 = [&] {
		a++; // right
		std::cout << "a = " << a << std::endl; // right
	};
	return 0;
}

其余部分

  1. 参数列表 (parameter list):

    • 定义lambda函数的参数。
    • 参数列表与常规函数相同,可以包含多个参数。
    • 如果没有参数,可以省略不写。
  2. mutable 关键字:

    • 如果lambda函数需要修改捕获的变量,则需要使用mutable关键字。
    • 不使用时,可省略。
  3. 返回类型 (return type):

    • 可以显式指定lambda函数的返回类型。
    • 如果省略返回类型,编译器会自动推导返回类型。
  4. 函数体 (function body):

    • 包含lambda函数的执行代码(与函数体编写一样)。

使用

以下是一些lambda表达式的使用示例:

示例1:简单的lambda函数

#include <iostream>
#include <functional>

int main() 
{
    int x = 10;

    // 值捕获
    auto add = [=](int a, int b) {
        return a + b + x; // 注意这里x是常量
    };
    std::cout << add(5, 3) << std::endl; // 输出18

    // 引用捕获
    auto multiply = [&x](int a, int b) mutable {
        ++x; // 修改外部变量
        return a * b * x;
    };
    std::cout << multiply(5, 3) << std::endl; // 输出165

	// 示例3
	int a = 1;
	int b = 2;
	auto swap = [](int& a, int& b) {
		int tmp = a;
		a = b;
		b = tmp;
	};
	swap(a, b);
	std::cout << "a = " << a << ", b = " << b << std::endl;
	
	// 示例4
	auto func = [] {
		std::cout << "无参匿名函数" << std::endl;
	};
	func();
    return 0;
}

示例2:使用lambda表达式排序

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};

    // 使用lambda表达式对vector排序
    std::sort(vec.begin(), vec.end(), [](int a, int b) {
        return a > b; // 降序排列
    });

    for (int num : vec) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

示例3:lambda作为比较器

#include <queue>
#include <iostream>

class Date 
{
public:
    Date() = default;
    Date(int year, int month, int day)
        :_year(year), _month(month), _day(day)
    {}
    bool operator<(const Date& d2) const {
        return _year < d2._year;
    }

    void print() const {
        std::cout << _year << "-" << _month << "-" << _day << std::endl;
    }

private:
    int _year = 2024;
    int _month = 9;
    int _day = 25;
};

int main() {
    auto DateLess = [](const Date* d1, const Date* d2) {
        return *d1 < *d2;
    };

    std::priority_queue<Date*, std::vector<Date*>, decltype(DateLess)> pq(DateLess);

    Date date1, date2(2024, 9, 26);
    

    pq.push(&date1);
    pq.push(&date2);

    while (!pq.empty()) {
        Date* top = pq.top();
        top->print();
        pq.pop();
    }

    return 0;
}

分析:

传递比较器的类型

这里使用 Lambda 表达式作为std::priority_queue比较器,需要使用 decltype 推导出该对象的类型,将比较器的类型 std::priority_queue 的模板进行实例化。

template <class T, class Container = vector<T>, class Compare = less<typename Container::value_type> > 
 class priority_queue;

注意与std::sort要传入比较对象不同:

template <class RandomAccessIterator, class Compare>
  void sort (RandomAccessIterator first, RandomAccessIterator last, Compare comp);

还要传入比较对象

因为 decltype(DateLess) 实际上是指定了一个 lambda 函数的类型,而 lambda 函数没有默认构造函数,因此不能直接用作模板参数来实例化一个需要默认构造其比较函数的容器(如 std::priority_queue)。

因此,当你试图使用 decltype(DateLess) 类型来创建一个对象时,如果没有提供具体的 lambda 实例,就会出现问题。

std::priority_queue 的构造函数有两种常见的方式:

  1. 默认构造函数:创建一个空的优先队列,通常需要提供比较函数类型即可,然后会再调用这个类型的默认构造。
  2. 带比较函数的拷贝构造函数:允许你在创建优先队列时立即提供一个比较函数对象。

由于 decltype(DateLess) 是一个函数类型,而 lambda 函数没有默认构造函数,所以不能调用std::priority_queue的默认构造进行构造其对象。

只能调用其拷贝构造函数,因此你必须在创建 std::priority_queue 时显式地提供一个比较函数对象,如你所示:

std::priority_queue<Date*, std::vector<Date*>, decltype(DateLess)> pq(DateLess);

这样做的原因是,std::priority_queue 的构造函数会接受一个比较函数作为参数,并且会调用该函数的拷贝构造函数或移动构造函数来初始化优先队列内部使用的比较器。

推导类型时不能使用 typeid

如果你试图使用 typeid(DateLess),那么你需要明白它返回的是 std::type_info 对象,而不是函数类型。下面是一个示例:

auto DateLess = [](const Date* d1, const Date* d2) {
    return d1 < d2;
};

std::type_info ti = typeid(DateLess);

// 打印类型信息(仅用于调试)
std::cout << "Type of DateLess: " << ti.name() << std::endl;

这种方式主要用于调试目的,可以输出类型信息的字符串表示。但请注意,ti.name() 返回的字符串通常是实现定义的,可能在不同的编译器或平台上有不同的形式。

使用 typeid(DateLess) 本身不会直接提供你所需的类型信息来作为 std::priority_queue 的比较函数类型。typeid(DateLess) 返回的是一个 std::type_info 对象,而不是函数类型本身。

std::type_info 主要用于运行时类型信息(RTTI),可以用来进行类型检查或动态绑定等操作。它提供了类型的信息,但不是你想要的函数类型。

如果你确实想在代码中使用 typeid 来获取类型信息,通常是为了调试或者类型检查的目的,而不是为了类型推导或模板参数。

推导类型时不能使用 typeid.name()

typeid(T).name() 提供了获取类型的字符串表示的方法。这通常用于运行时类型信息(RTTI)处理,可以用来进行类型检查或动态绑定等操作。typeid(T).name() 返回的是一个 const char* 类型的字符串,表示类型的名称。

让我们对比一下 decltypetypeid.name() 在此场景下的不同用途:

  1. decltype(DateLess):

    • 用于确定 DateLess 的类型,这个类型可以用于模板参数或类型别名。
    • 这是一个编译期的操作,编译器会在编译期间解析 DateLess 的类型。
    • 用于静态类型信息处理。
  2. typeid(DateLess).name():

    • 用于获取 DateLess 类型的名字作为一个字符串。
    • 这是一个运行期的操作,需要在程序执行时才能得到结果。
    • 通常用于运行时类型信息,比如打印类型名字或者做类型检查。

在上述代码中,我们不能使用 typeid(DateLess).name() 来代替 decltype(DateLess),因为 std::priority_queue 需要的是一个实际的类型而不是一个类型的名字字符串。因此,decltype 更加适合这里的情景,因为它提供了正确的类型信息,而不仅仅是类型的名称。

如果尝试使用 typeid(DateLess).name() 来创建 std::priority_queue,会因为类型不匹配导致编译错误,因为 typeid(DateLess).name() 返回的是一个 const char*,而 std::priority_queue 期望的是一个可以调用的对象类型。

typename的使用

typename 关键字主要用于在模板编程中解决依赖类型的问题。当模板参数类型影响到另一个类型时,编译器可能无法立即确定这个类型,这时就需要使用 typename 来明确告诉编译器这是一个类型名。

使用场景

  1. 模板中的类型名:在模板定义中,如果模板参数的类型决定了另一个类型的名称,而该类型在模板外部是未知的,此时需要使用 typename 来指示这是一个类型。

    template<typename T>
    class MyClass {
    public:
        typename T::SubType* getSubType() {
            // ...
        }
    };
    

    在上面的例子中,T::SubType 是一个依赖于模板参数 T 的类型,因此需要使用 typename

  2. 当你在类模板中引用一个内嵌类型(nested type),而这个类型名可能与模板参数或其他成员(如静态成员变量)的名字冲突时,编译器可能无法直接确定这个名称是指代一个类型还是一个静态成员变量。在这种情况下,你需要使用 typename 关键字来明确告诉编译器这个名称是一个类型。

示例

template<typename T>
class MyClass {
public:
    // 静态成员变量
    static T value;

    // 内嵌类型
    typedef int AnotherType;

    // 成员函数,演示如何内部使用类型
    void func() {
        AnotherType a = 0; // 内部使用,不需要 typename
    }
};

// 假设我们需要在模板外部引用 AnotherType
// 这里需要 typename 来告诉编译器 AnotherType 是一个类型
typename MyClass<int>::AnotherType x = 5;

// 注意:静态成员变量 value 的引用不需要 typename
int y = MyClass<int>::value;

在上面的例子中,MyClass 是一个模板类,它有一个静态成员变量 value 和一个内嵌类型 AnotherType

  • 在模板的外部,当我们想要引用 AnotherType 时,需要使用 typename 关键字来明确它是一个类型。
  • 而对于静态成员变量 value 的引用,则不需要 typename,因为静态成员变量的引用是通过点操作符(.->)来解析的,这种解析方式已经足够让编译器区分出它不是一个类型。

底层

Lambda表达式在底层实际上是一个特殊的类实例,这个类具有捕获列表、参数列表、返回类型以及函数体。

  1. 编译器生成的类

    当你定义一个Lambda表达式时,编译器会自动生成一个类,这个类包含了Lambda函数的所有信息,包括捕获的变量、函数的参数、返回类型以及函数体。

  2. 捕获列表

    捕获列表指定了Lambda函数可以访问哪些外部变量。这些变量可以通过值捕获([=])、通过引用捕获([&])或者混合捕获(如[&, var1])。编译器会根据捕获列表生成相应的成员变量。

  3. 操作符()重载

    编译器生成的类会有一个重载的operator()成员函数,这个函数对应于Lambda表达式的函数体。每次调用Lambda表达式时,实际上是调用了这个重载的operator()

  4. 临时对象

    当Lambda表达式被定义时,编译器会创建一个这个类的临时对象,并且初始化捕获列表中的变量。这个临时对象就是Lambda表达式本身。

示例分析

让我们来看一个具体的例子,看看编译器如何处理Lambda表达式:

int main() {
    int x = 10;
    auto add = [=](int a, int b) {
        return a + b + x;
    };

    std::cout << add(5, 3) << std::endl; // 输出18

    return 0;
}

假设编译器生成的Lambda类如下所示(这是一个简化的例子,实际生成的类会更复杂):

struct __lambda_0 {
    int _x;

    __lambda_0(int x) : _x(x) {}

    int operator()(int a, int b) const {
        return a + b + _x;
    }
};

在这个例子中,编译器生成了一个名为__lambda_0的类。这个类有一个成员变量_x,它通过值捕获了外部变量x。类还包含了一个构造函数,用于初始化捕获的变量。最后,类重载了operator(),对应于Lambda表达式的函数体。

Lambda表达式的创建过程

add被定义时,编译器实际上执行了以下步骤:

  1. 创建一个__lambda_0类型的临时对象。
  2. 使用x的值初始化_x成员变量。
  3. 将这个临时对象赋值给add

因此,add实际上是一个__lambda_0类型的对象,调用add(5, 3)相当于调用__lambda_0类的operator()(5, 3)方法。

总结出一句换就是,Lambda 表达式底层是 仿函数

两个表达式对象不能进行赋值

尽管两个lambda的函数体内容相同,但不能进行赋值操作。因为一个lambda表达式是唯一的类,类型不同的两个对象不能进行赋值:

int main()
{
	auto f1 = [] { std::cout << "hello world" << std::endl; };
	auto f2 = [] { std::cout << "hello world" << std::endl; };
	f1 = f2; // 编译失败--->提示找不到operator=()
	return 0;
}

在这里插入图片描述
从反汇编可以看到,两者是不同的类型:
在这里插入图片描述


今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值