【C++进阶知识】03 - lambda表达式

1 捕获列表

(1)在lambda表达式的捕获列表只能捕获非静态的局部变量,对于全局变量和静态变量,可以在lambda表达式函数体中直接进行使用。

// 全局变量
int l = 0;

int main()
{
	// 静态变量
	static int s = 0;
	auto f = []() {
		s = 100;
		l = 50;
	};
	return 0;
};

并且在lambda中进行修改也是同样有效。

(2)按值捕获时,可以通过mutable开启内部的修改性,而修改后下次再调用该lambda表达式的时候,数值会是修改之后的。

int main()
{
	int i = 0;
	auto f = [i]() mutable 
	{
		i++;
		std::cout << "lambda: " << i << std::endl;
	};
	f();
	f();
	f();
	std::cout << "Main Fun: " << i << std::endl;
	return 0;
};

【C++进阶知识】03 - lambda表达式_第1张图片

(3)不需要捕获const int变量

如果该变量是const非volatile的整型或枚举类型,并且已经常量表达式初始化,那么不需要在捕获列表中捕获如果声明为constexpr那么就算不是int也能直接捕获。

/** 正确,const int 类型能在lambda中直接捕获 */
const int i = 10;
auto f = [] ()
{
	return i;
};
/** 正确,const int 类型能在lambda中直接捕获 */
constexpr int i = 10;
auto f = [] ()
{
	return i;
};
/** 错误,只能不捕获const int */
const float d = 10.;
auto f = [] ()
{
	return d;
};
/** 正确,constexpr也能直接不捕获 */
constexpr float d = 10.;
auto f = []()
{
	return d;
};

注意:此处的不捕获是针对于仅读取值,如果需要使用,那么一定要进行捕获。

constexpr float d = 10.;
auto f = [=]()
{
	// 进行使用
	return &d;
};

(4)捕获的变量大小

还需注意的是,如果前面捕获了一堆东西,但是在lambda中没有进行使用,相当于你什么都没有捕获。

/** 没有进行使用 */
int a;
int b;
int c;
int d;
auto f = [=]()
{
	return 1;
};
std::cout << sizeof f << std::endl;  // 输出 1

/** 对a,b进行使用,返回 a + b 的大小 */
int a;
int b;
int c;
int d;
auto f = [=]()
{
	int w = a + b;
	return w;
};
std::cout << sizeof f << std::endl;  // 输出 8

2. lambda表达式的实现原理

我们将lambda表达式解开,看看它内部是怎么做的:

/** 原来的式子 */
int main()
{
	int i = 0;
	int j = 0;
  	static int s = 0;
	auto f = [i, &j]()
	{
		std::cout << i + 1 << std::endl;
		std::cout << j + 1 << std::endl;
		std::cout << s + 1 << std::endl;
	};
	f();
	return 0;
};
/** 解开lambda后 */
int main()
{
	int i = 0;
  	// 声明一个class,专门供lambda表达式使用
  	class __lambda_8_11
  	{
    	public: 
    		// 使用inline表达式,把lambda的函数体移到()的重载当中
    		inline /*constexpr */ void operator()() const
    		{
      			std::cout.operator<<(i + 1).operator<<(std::endl);
      			std::cout.operator<<(j + 1).operator<<(std::endl);
      			// 静态或全局的在此处可以直接使用
      			std::cout.operator<<(s + 1).operator<<(std::endl);
    		}
    
   		private:
   			// 根据捕获参数声明成私有成员变量 
    		int i;
    		int & j;
    
    	public:
    		// 在构造函数中传入捕获列表的参数
    		__lambda_8_11(int & _i, int & _j)
    			: i{_i}
    			, j{_j}
    		{}
    
  	};
  	// 用{}调用构造函数
  	__lambda_8_11 f = __lambda_8_11{i, j};
  	// 调用()重载函数
  	f.operator()();
  	return 0;
}

注意:
(1)默认生成的operator()() const是带有const的,如果你的lambda声明为mutable,那么函数将会变为operator()()
(2)怎么转化为一个函数指针,其实是在内部创建了一个static的返回函数,在里面直接返回了函数指针。
(3)模板的实现,是将里面的函数定义成了模板函数,来实现对应泛型的效果。

3. C++标准对lambda表达式的优化

3.1 无状态lambda表达式的优化

上面我们看到了每使用一个lambda表达式,就会生成一个class,因此C++标准对无状态的lambda表达式(即没有捕获任何外部的参数),那么它可以隐式的转化为函数指针,这样减少类的创建,减少性能开销。

void f(void(*)())
{
};

void g(void(&)())
{
};

int main()
{
	f([] {});
	g(*[] {});
	return 0;
};

3.2 广义捕获

在C++14中引入广义捕获的概念,这使得在捕获列表中可以设置一个变量将表达式的值进行拷贝。

应用场景:

(1)对将要销毁的值在捕获列表中进行拷贝。

class A
{
private:
	int value;
public:
	A() : value(10) {}
	auto fun()
	{
		auto b = [this]() -> int
		{
			return value;
		};
		return b;
	}
};
// 返回销毁后的A对象的捕获列表
auto fun()
{
	A a;
	return a.fun();
}
// 因为A对象的销毁,此处访问会造成严重的内存泄漏
int main()
{
	A a;
	auto f = fun();
	int i = f();
	std::cout << i << std::endl;
};

输出结果如下:

【C++进阶知识】03 - lambda表达式_第2张图片

而在C++14中 ,我们可以对this进行拷贝并返回,这样就不会出问题了。

class A
{
private:
	int value;
public:
	A() : value(10) {}
	auto fun()
	{
		// 捕获列表中拷贝this对象,就算该对象被析构,lambda创建的这个类是一值存在的
		auto b = [ca = *this]() -> int
		{
			return ca.value;
		};
		return b;
	}
};

auto fun()
{
	A a;
	return a.fun();
}

int main()
{
	A a;
	auto f = fun();
	int i = f();
	std::cout << i << std::endl;
};

【C++进阶知识】03 - lambda表达式_第3张图片

而在C++17中进行了进一步的优化,访问this只需要在捕获列表中使用*this即可,而this已经不是原来的this了。

class A
{
private:
	int value;
public:
	A() : value(10) {}
	auto fun()
	{
		// 在C++17中只使用*this即可
		auto b = [*this]() -> int
		{
			return this->value;
		};
		return b;
	}
};

正因为这个原因,在C++20中为了区分*thisthis对语法的格式进行了要求:

/** =虽然可以捕获this,但是C++20强制要求显式捕获 */
[=]() -> int
{
	return this->value;
};
/** 正确的格式 */
[=, this]() -> int
{
	return this->value;
};

(2)在捕获列表中使用移动构造减少性能开销

std::string s = "test";
auto f = [x = std::move(s)] ()
{
	// ......
};

3.3 泛型lambda表达式

(1)在参数列表中使用auto,达到泛型的效果。

auto f = [] (auto x)
{
	return x;
};

(2)lambda表达式对模板的支持

C++20之后,在捕获列表之后加入,达到泛型编程的作用。

auto f = [] <typename T> (std::vector<T> vector)
{
	// .....
}

3.4 可构造、可赋值的无状态lambda表达式

因为上面说了lambda表达式其实是一个函数指针,因此lambda表达式的默认构造函数和赋值构造函数都被删除了。在C++20中,允许了无状态的lambda表达式的默认构函数和默认赋值函数。

auto f = [](auto x, auto y)
{
	return x > y;
};
/** map的比较中需要传入一个类型并作为模板传入模板 */ 
// 构造允许
std::map<std::string, int, decltype(f)> Map1, Map2;
// 赋值允许
Map2 = Map1;

你可能感兴趣的:(C++专栏,c++,开发语言)