很久之前总结过一篇C++11/14 lambda表达式使用及解析,其大体介绍了C++11/14中lambda表达式的基本使用,关于其他高级特性/使用等均未涉及;因此本文继该文章,进一步总结lambda表达时的高级特性(包括基本属性,细节)和使用。
由于目前C++20已经发布,因此,计划总结成如下系列:
C++11/14 lambda表达式使用及解析(二)主要总结C++11中的lambda特性及使用。
C++11/14 lambda表达式使用及解析(三)主要总结C++14中的lambda特性及使用。
C++11/14/17 lambda表达式使用及解析(四)主要总结C++17中的lambda特性及使用。
C++17/20 lambda表达式使用及解析(五)主要总结C++20中lambda特性及使用。
上述三、四、五目前未完成,本篇主要旨在完成二。
在开始本文之前,先给出本文代码测试所用的环境(机器信息以及gcc版本)
uname -a
Linux qls-VirtualBox 5.11.0-37-generic #41~20.04.2-Ubuntu SMP Fri Sep 24 09:06:38 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)
然后,我们由一个在现代C++标准中使用lambda表示式经常会碰到的一个问题:
即当lambda表达式按值捕获某个变量a,在其函数体中修改a,会报错!
具体可参考如下代码:
int main() {
int a = 0;
auto b = [a]() {
++a;
};
b();
return 0;
}
构建上述代码后,编译器给出如下输出
test.cc: In lambda function:
test.cc:8:7: error: increment of read-only variable ‘a’
8 | ++a;
| ^
而当我们按引用捕获a时,便可正常work!
关于该问题的原因和解决方案,会在本文高级使用场景中给出答案。
下面开始进入正文,从lambda表达式的基本概念说起,其也可参考:C++11/14 lambda表达式使用及解析,本文的基础概念部分是对该文的进一步补充。
闭包/闭包类型名称参考自Effective Modern C++ 简体中文版。
关于何为闭包,C++标准给出的定义如下expr.prim.lambda#2
The evaluation of a lambda-expression results in a prvalue temporary. This temporary is called the closure object .
上述定义可推出如下两点(个人理解):
关于什么是prvalue,其在Value categories - cppreference.com中有如下定义
a prvalue (“pure” rvalue) is an expression whose evaluation
- computes the value of an operand of a built-in operator (such prvalue has no result object), or
- initializes an object (such prvalue is said to have a result object).
The result object may be a variable, an object created by new-expression, a temporary created by temporary materialization, or a member thereof. Note that non-void discarded expressions have a result object (the materialized temporary). Also, every class and array prvalue has a result object except when it is the operand of decltype;
简言之:一个prvalue是一个表达式,其执行的结果用在
关于prvalue本文仅介绍到此,详细可参考
Value categories - cppreference.com
c++ - What are rvalues, lvalues, xvalues, glvalues, and prvalues? - Stack Overflow
C++ Templates(第2版 英文版)
关于何为闭包类型,C++标准给出定义如下[expr.prim.lambda]
The type of the lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type — called the closure type .
由此可知:闭包类型主要是闭包对象的类类型。
在介绍完基本概念后,下面便进行lambda表达式相应属性介绍。
首先,本文会通过C++ Insights观察编译器对lambda表达式的扩展,在C++11/14 lambda表达式使用及解析中给出过无状态lambda表达式的扩展,因此,此处不再给出具体示例。
在该部分,主要表述一个观点:纵使两个lambda表达式实现一样,其闭包类型也不同。
可参考如下代码理解
int main() {
std::cout << std::boolalpha;
auto lb1 = []() {};
auto lb2 = []() {};
std::cout << "lb1 and lb2 type same: "
<< std::is_same::value << "\n";
return 0;
}
上述结果会显示为false。
在标准[expr.prim.lambda] 中有关于闭包类的构造函数等规定有如下定义
The closure type associated with a lambda-expression has a deleted ([dcl.fct.def.delete]) default constructor and a deleted copy assignment operator.
也即,针对闭包类,其构造函数和拷贝赋值函数等是delete的。
因此在C++11中,不能通过decltype(闭包) 默认声明一个闭包。也即不能实现如下操作
decltype(lb1) lb3;
标准并没有说拷贝构造函数为delete,因此可以拷贝闭包,且拷贝的闭包和被拷贝的闭包其闭包类型相同。
具体可参考如下代码
int main() {
std::cout << std::boolalpha;
auto lb1 = []() {};
auto lb2 = lb1;
std::cout << "lb1 and lb2 type same: "
<< std::is_same::value << "\n";
return 0;
}
上述结果输出为true。
关于捕获列表的定义,可参考该系列第一篇,本文不赘述。在本文中,将捕获归为无状态捕获,有状态捕获两大类,相对应为无状态闭包和有状态闭包。
其有状态捕获又可归为如下如下几类:
关于1,2,3,4相关的说明和示例可参考C++11/14 lambda表达式使用及解析,本文会讲解5,6并给出相应示例。
关于lambda捕获全局变量/静态变量,捕获全局变量,有些编译器可能会报错,所以最好不要这样做;针对静态变量捕获,其lambda表达式并不会产生拷贝,也相当于未捕获,所以最好也不要这样做。相关示例,本文略过。
下面,便详细讲解文章开头给出的例子:即lambda表达式捕获变量a,按引用捕获b,在其函数体中修改a会报错,修改b可以通过的问题。
首先看当lambda表达式按值捕获a的情景,参考如下代码
int main() {
int a = 1;
auto lb1 = [a]() { ++a; };
lb1();
std::cout << "a= " << a << "\n";
return 0;
}
构建上述代码,编译器报告如下错误
test.cc: In lambda function:
test.cc:8:24: error: increment of read-only variable ‘a’
8 | auto lb1 = [a]() { ++a; };
|
编译器竟然报变量a为一个只读变量。
通过C++ Insights观察上述lambda表达式经编译器后扩展为如下实现
class __lambda_7_13
{
public:
inline void operator()() const
{
++a;
}
private:
int a;
public:
// inline /*constexpr */ __lambda_7_13(__lambda_7_13 &&) noexcept = default;
__lambda_7_13(int & _a)
: a{_a}
{}
};
由上述生成代码可知,
因此,由上述说明便可知,lambda表达式不可以修改按值捕获的变量,因为operator() 被编译器默认扩展为const属性,因此不能在该接口中修改闭包类的成员。
解决该问题,最直观的方式是将operator()接口的const修饰符去掉,或者将闭包类中的成员变量a声明为mutable!
在C++11中,可将关键词mutable应用lambda表达式,这样会使编译器扩展的operator()为不带const属性的接口,便可以修该被捕获的变量!
关于按值捕获,标准中也给出了相应的定义[expr.prim.lambda]
When the lambda-expression is evaluated, the entities that are captured by copy are used to direct-initialise each corresponding non-static data member of the resulting closure object.
接下来看一看当按引用捕获变量a时,编译器的行为,示例代码如下
int main() {
int a = 1;
auto lb1 = [&a]() { ++a; };
lb1();
std::cout << "a= " << a << "\n";
return 0;
}
上述代码能正常通过,且能输出正确的结果。
通过C++ Insights观察上述lambda表达式生成的代码,其结果如下
class __lambda_7_13
{
public:
inline void operator()() const
{
++a;
}
private:
int & a;
public:
// inline /*constexpr */ __lambda_7_13(__lambda_7_13 &&) noexcept = default;
__lambda_7_13(int & _a)
: a{_a}
{}
};
__lambda_7_13 lb1 = __lambda_7_13(__lambda_7_13{a});
由上述代码可得出如下两点结论
关于为什么在const 属性的operator()接口中可以修改引用变量a,需要搞明白顶层const与底层const的概念,相应的概念可参考C++ Primer 中文版(第 5 版)相应章节的内容,此处不细讲。
简单来说,成员函数const属性作用于成员变量相当于底层const,譬如指针int *p,为成员变量,这在const 属性成员函数中相当于声明int *const p, 即无法修改p,但可以修改*p;由于引用不存在底层const(在成员函数中), 因此,const属性对其没有影响,故可以修改引用成员变量。
关于解释也可参考:Modifying reference member from const member function in C++ - Stack Overflow
至此, 关于文章开头的问题已经讲解分析完毕。
简言之,当lambda表达式捕获一个const属性的变量a,则a在lambda表达式生成的函数体中,保持相应的const属性,其可以通过如下代码验证
int main() {
std::cout << std::boolalpha;
const int a = 1;
auto lb1 = [a]() {
std::cout << "a is const: " << std::is_const::value << "\n";
};
lb1();
return 0;
}
其输出结果为true。
针对该种对象的捕获,C++11中只能通过按引用捕获实现该种类型的对象捕获。
本部分,仅讲解按值捕获模版参数包。lambda表达式可以通过在捕获列表中声明[args...]这种形式,进行可变参数捕获。
但在C++11中,如果需要使用这些捕获的参数,这需要一些辅助函数。
按值捕获参数包,可参考如代码
template
void captureTest(Args... args) {
auto lb = [args...] () {
auto tpl = std::make_tuple(args...);
std::cout << "tpl size: " << std::tuple_size::value << "\n";
};
lb();
}
int main() {
captureTest(1.0, 2.0, "hello world");
return 0;
}
上述代码输出结果为 3。
关于C++11中lambda表达式的属性总结,便到此结束。
下面进入高级使用特性。
在C++11/14 lambda表达式使用及解析中 lambda与pthread_create的例子小节给出了一个lambda表达式转换为函数指针的例子。其给出了粗略的解释。下面给出更详尽的解释。
只有无状态闭包才能转换为函数指针,也即没有捕获任何变量的lambda表达式才能转换为相应的函数指针。
其在标准中也给出了如下定义[expr.prim.lambda]
The closure type for a lambda-expression with no lambda-capture has a public non- virtual non-explicit const conversion function to pointer to function having the same parameter and return types as the closure type’s function call operator. The value returned by this conversion function shall be the address of a function that, when invoked, has the same effect as invoking the closure type’s function call operator.
看一个来自C++11/14 lambda表达式使用及解析的例子,其lambda表达式如下
auto f = [] (int x) {return 42;};
通过C++ Insights观察到上述代码生成如下的类定义:
class __lambda_6_11
{
public:
inline /*constexpr */ int operator()(int x) const
{
return 42;
}
using retType_6_11 = int (*)(int);
inline /*constexpr */ operator retType_6_11 () const noexcept
{
return __invoke;
};
private:
static inline int __invoke(int x)
{
return 42;
}
};
由上述生成代码可知:
上述便为 无状态lambda表达式转为函数指针的全部内容。因此在pthread_create中,我们需要用无状态lambda表达式,且只能用该种表达式。
由于lambda表达式会被编译器扩展为闭包类,故该闭包类当然也可作为继承体系中的基类。
在C++11种,由于无法直接获得闭包类,因此需要利用template获得相应的闭包类。具体可参考如下代码
template
class Derived : public Callable {
public:
Derived(Callable callable) : Callable(callable) {}
};
template
Derived makeDerived(Callable&& f) {
return Derived(std::forward(f));
}
int main() {
auto lb = makeDerived([](int i) {
std::cout << "hello world! \n";
});
lb(0);
return 0;
}
上述中makeDerived函数模版可看作为一个功能函数模版,主要是为了获得lambda表达式的闭包类,然后利用该类作为基类,构造Derived
当然,你也可以继承多个闭包类,感兴趣可以自己实现。
很通用的场景,便是将一类lambda表达式存储在std::vector容器中。
在C++11中,我们一般通过std::function,将lambda表达式存储于vector容器中。
针对同类型的无状态lambda表达式,其可以直接存储于vector容器中,这是因为无状态lambda表达式会转换为函数指针,因此此时vector容器中,存储的为相应的同类型的函数指针。
在C++11中,如果你写如下的代码,编译器会报错
int main() {
auto lb = []() {
};
std::vector vc;
vc.push_back([](){});
return 0;
}
原因我们在本文中已经讲过,此处赘述一遍:每个lambda表达式所生成的闭包的类型都是唯一的且互不相同,而vector中需要存储相同类型的闭包,故上述代码不能通过编译。
本文基本概括了C++11中lambda表达式需要了解的知识,本文未讲解lambda表达在多线程环境下的表现。如果发现本文有什么不对的地方,也欢迎补充指正。
本文主要参考如下书籍内容总结而出
C++ Lambda Story: Everything you need to know about Lambda Expressions in Modern C++!
Effective Modern C++ 简体中文版
C++ Templates(第2版 英文版)
C++ Primer 中文版(第 5 版)