朋友们,下午好。 今天,我们为您准备了文章
“ Lambdas:从C ++ 11到C ++ 20”的第一部分的翻译。 该材料的发布时间恰好与明天开始的
“ C ++开发人员”课程的发布时间相同。
Lambda表达式是C ++ 11中最强大的功能之一,并且随着每种新语言标准的发展而不断发展。 在本文中,我们将回顾他们的历史,并研究现代C ++这个重要部分的演变。

第二部分在这里可用:
Lambda:从C ++ 11到C ++ 20,第2部分参赛作品在本地C ++用户组会议上,我们就lambda表达式的“历史”进行了现场编程。 对话由C ++专家TomaszKamiński主持(
请参阅Thomas的Linkedin个人资料 )。 这是事件:
Lambda:从C ++ 11到C ++ 20-C ++用户组克拉科夫我决定从Thomas(经过他的许可!)获取代码,对其进行描述并创建另一篇文章。
我们将从探索C ++ 03和对紧凑局部函数表达式的需求开始。 然后我们转到C ++ 11和C ++ 14。 在本系列的第二部分中,我们将看到C ++ 17中的变化,甚至看看C ++ 20中将发生什么。
C ++ 03中的Lambda从一开始,STL的
std::algorithms
(例如
std::sort
)就可以接受任何被调用的对象,并在容器元素上对其进行调用。 但是,在C ++ 03中,这仅涉及指向函数和函子的指针。
例如:
#include <iostream> #include <algorithm> #include <vector> struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; int main() { std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); }
运行代码:
@Wandbox但是问题在于,您必须在不同的范围内而不是在算法调用的范围内编写一个单独的函数或函子。
作为一种潜在的解决方案,您可能考虑编写本地仿函数类-因为C ++始终支持此语法。 但这行不通...
看一下这段代码:
int main() { struct PrintFunctor { void operator()(int x) const { std::cout << x << std::endl; } }; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), PrintFunctor()); }
尝试使用
-std=c++98
编译,您将在GCC中看到以下错误:
error: template argument for 'template<class _IIter, class _Funct> _Funct std::for_each(_IIter, _IIter, _Funct)' uses local type 'main()::PrintFunctor'
本质上,在C ++ 98/03中,您不能创建具有本地类型的模板的实例。
由于所有这些限制,委员会开始开发一项新功能,我们可以创建该功能并将其称为“就地” ...“ lambda表达式”!
如果我们看一下
N3337 -C ++ 11的最终版本,我们将看到lambda的单独部分:
[expr.prim.lambda] 。
紧接C ++ 11我认为lambda已被明智地添加到语言中。 他们使用新的语法,但是编译器随后将其“扩展”为真实的类。 因此,我们拥有真正的严格类型化语言的所有优点(有时是缺点)。
这是一个基本代码示例,还显示了相应的本地仿函数对象:
#include <iostream> #include <algorithm> #include <vector> int main() { struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance; std::vector<int> v; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), someInstance); std::for_each(v.begin(), v.end(), [] (int x) { std::cout << x << '\n'; } ); }
示例:
@WandBox您还可以签出CppInsights,它显示了编译器如何扩展代码:
看一下这个例子:
CppInsighs:Lambda测试在此示例中,编译器将转换:
[] (int x) { std::cout << x << '\n'; }
变成与此类似的形式(简化形式):
struct { void operator()(int x) const { std::cout << x << '\n'; } } someInstance;
Lambda表达式语法:
[] () { ; } ^ ^ ^ | | | | | : mutable, exception, trailing return, ... | | | |
我们开始之前的一些定义:
来自
[expr.prim.lambda#2] :
计算lambda表达式会导致一个临时的prvalue。 此临时对象称为
闭包对象 。
并且来自
[expr.prim.lambda#3] :
lambda表达式的类型(也是闭包对象的类型)是称为
闭包类型的类的唯一无名非工会
类型 。
Lambda表达式的一些示例:
例如:
[](float f, int a) { return a*f; } [](MyClass t) -> int { auto a = t.compute(); return a; } [](int a, int b) { return a < b; }
λ型由于编译器会为每个lambda生成一个唯一的名称,因此不可能事先知道它。
auto myLambda = [](int a) -> double { return 2.0 * a; }
此外
[expr.prim.lambda] :
与lambda表达式关联的闭包类型具有一个远程([dcl.fct.def.delete])默认构造函数和一个远程赋值运算符。
因此,您不能写:
auto foo = [&x, &y]() { ++x; ++y; }; decltype(foo) fooCopy;
这会在GCC中导致以下错误:
error: use of deleted function 'main()::<lambda()>::<lambda>()' decltype(foo) fooCopy; ^~~~~~~ note: a lambda closure type has a deleted default constructor
呼叫接线员放入lambda主体中的代码被“转换”为相应闭包类型的operator()代码。
默认情况下,这是一个内置的常量方法。 您可以在声明参数后通过指定mutable来更改它:
auto myLambda = [](int a) mutable { std::cout << a; }
尽管常量方法不是没有空捕获列表的lambda的“问题”,但是当您要捕获某些东西时,它就很重要。
捕捉[]不仅引入了lambda,而且还包含捕获变量的列表。 这称为捕获列表。
通过捕获变量,您可以在闭包类型中创建此变量的副本成员。 然后,可以在lambda主体内访问它。
基本语法为:
- [&]-通过引用捕获,自动存储中的所有变量都在作用域中声明
- [=]-按值捕获,将值复制
- [x,&y]-按值显式捕获x,按引用显式捕获y
例如:
int x = 1, y = 1; { std::cout << x << " " << y << std::endl; auto foo = [&x, &y]() { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl; }
您可以在此处
尝试完整的示例:
@Wandbox尽管指定
[=]
或
[&]
会很方便-由于它会捕获自动存储中的所有变量,所以显式地捕获变量更为明显。 因此,编译器可以警告您不必要的影响(例如,请参见有关全局和静态变量的说明)
您还可以在Scott Meyers撰写的有效现代C ++第31段中了解更多信息:“避免使用默认捕获模式。”
还有一个重要的报价:
C ++闭包不会增加捕获链接的生存期。
可变的默认情况下,闭包类型运算符()是常量,您不能在lambda表达式主体内修改捕获的变量。
如果要更改此行为,则需要在参数列表之后添加mutable关键字:
int x = 1, y = 1; std::cout << x << " " << y << std::endl; auto foo = [x, y]() mutable { ++x; ++y; }; foo(); std::cout << x << " " << y << std::endl;
在上面的示例中,我们可以更改x和y的值,但是这些只是所附范围中x和y的副本。
全局变量捕获如果您具有全局值,然后在lambda中使用[=],则您可能会认为全局值也可以通过值来捕获,但事实并非如此。
int global = 10; int main() { std::cout << global << std::endl; auto foo = [=] () mutable { ++global; }; foo(); std::cout << global << std::endl; [] { ++global; } (); std::cout << global << std::endl; [global] { ++global; } (); }
您可以在此处使用代码:
@Wandbox
仅捕获自动存储中的变量。 GCC甚至可能发出以下警告:
warning: capture of variable 'global' with non-automatic storage duration
仅当您明确捕获全局变量时才会出现此警告,因此,如果使用
[=]
,编译器将无济于事。
Clang编译器更有用,因为它会生成错误:
error: 'global' cannot be captured because it does not have automatic storage duration
参见
@Wandbox捕获静态变量捕获静态变量类似于捕获全局变量:
#include <iostream> void bar() { static int static_int = 10; std::cout << static_int << std::endl; auto foo = [=] () mutable { ++static_int; }; foo(); std::cout << static_int << std::endl; [] { ++static_int; } (); std::cout << static_int << std::endl; [static_int] { ++static_int; } (); } int main() { bar(); }
您可以在此处使用代码:
@Wandbox结论:
10 11 12
同样,仅当您显式捕获静态变量时才会出现警告,因此,如果使用
[=]
,编译器将无济于事。
班级成员捕获您知道执行以下代码后会发生什么:
#include <iostream> #include <functional> struct Baz { std::function<void()> foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); }
该代码声明一个Baz对象,然后调用
foo()
。 请注意,
foo()
返回一个lambda(存储在
std::function
),该lambda捕获该类的成员。
由于我们使用临时对象,因此无法确定调用f1和f2时会发生什么。 这是一个悬而未决的链接问题,导致未定义的行为。
类似地:
struct Bar { std::string const& foo() const { return s; }; std::string s; }; auto&& f1 = Bar{"ala"}.foo();
玩
@Wandbox代码
同样,如果您明确指定捕获([s]):
std::function<void()> foo() { return [s] { std::cout << s << std::endl; }; }
编译器将防止您的错误:
In member function 'std::function<void()> Baz::foo()': error: capture of non-variable 'Baz::s' error: 'this' was not captured for this lambda function ...
查看示例:
@Wandbox只能移动的对象如果您有一个只能移动的对象(例如,unique_ptr),则不能将其作为捕获变量放入lambda中。 按值捕获不起作用,因此您只能按引用捕获...但是,这不会将其转让给您,并且可能不是您想要的。
std::unique_ptr<int> p(new int[10]); auto foo = [p] () {};
保存常数如果捕获一个常量变量,则将保留常量:
int const x = 10; auto foo = [x] () mutable { std::cout << std::is_const<decltype(x)>::value << std::endl; x = 11; }; foo();
参见代码:
@Wandbox返回类型在C ++ 11中,您可以跳过
trailing
返回的lambda类型,然后编译器将为您输出它。
最初,返回值类型的输出仅限于包含一个return语句的lambda,但由于实现了更方便的版本没有问题,因此该限制很快被消除。
请参阅
C ++标准核心语言缺陷报告和已接受的问题 (感谢Thomas找到正确的链接!)
因此,从C ++ 11开始,如果所有返回语句都可以转换为相同类型,则编译器可以推断出返回值的类型。
如果所有return语句在左值到右值(7.1 [conv.lval]),数组到指针(7.2 [conv.array])和函数到指针(7.3 [conv。 func])与泛型类型相同;
auto baz = [] () { int x = 10; if ( x < 20) return x * 1.1; else return x * 2.1; };
您可以在此处使用代码:
@Wandbox上面的lambda中有两个
return
,但是它们都指向
double
,因此编译器可以推断类型。
IIFE-立即调用函数表达式在我们的示例中,我定义了一个lambda,然后使用闭包对象对其进行了调用……但也可以立即调用它:
int x = 1, y = 1; [&]() { ++x; ++y; }();
这样的表达式在常量对象的复杂初始化中很有用。
const auto val = []() { }();
我在
IIFE复杂初始化文章中写了更多有关此的
内容 。
转换为函数指针没有捕获的lambda表达式的闭包类型具有开放的非虚拟隐式函数,该函数将常量转换为指向具有与调用闭包类型的函数的运算符相同的参数和返回类型的函数的指针。 此转换函数返回的值必须是该函数的地址,该地址在调用时与调用类似于闭包类型的函数的运算符具有相同的作用。
换句话说,您可以将没有捕获的lambda转换为函数指针。
例如:
#include <iostream> void callWith10(void(* bar)(int)) { bar(10); } int main() { struct { using f_ptr = void(*)(int); void operator()(int s) const { return call(s); } operator f_ptr() const { return &call; } private: static void call(int s) { std::cout << s << std::endl; }; } baz; callWith10(baz); callWith10([](int x) { std::cout << x << std::endl; }); }
您可以在此处使用代码:
@WandboxC ++ 14的改进N4140标准和lambda:
[expr.prim.lambda] 。
C ++ 14对lambda表达式进行了两项重大改进:
这些功能解决了C ++ 11中可见的几个问题。
返回类型lambda表达式的返回值类型的输出已更新,以符合函数的自动输出规则。
[expr.prim.lambda#4]lambda的返回类型为auto,如果提供了返回的类型和/或从return语句推断出该值,则将其替换为尾随的返回类型,如[dcl.spec.auto]中所述。
用初始化器捕获简而言之,我们可以创建一个新的闭包类型的成员变量,然后在lambda表达式中使用它。
例如:
int main() { int x = 10; int y = 11; auto foo = [z = x+y]() { std::cout << z << '\n'; }; foo(); }
这可以解决几个问题,例如,仅可用于移动的类型。
搬家现在我们可以将对象移动到闭包类型的成员:
#include <memory> int main() { std::unique_ptr<int> p(new int[10]); auto foo = [x=10] () mutable { ++x; }; auto bar = [ptr=std::move(p)] {}; auto baz = [p=std::move(p)] {}; }
最佳化另一个想法是将其用作潜在的优化技术。 不必每次调用lambda都计算一些值,而是可以在初始化程序中计算一次:
#include <iostream> #include <algorithm> #include <vector> #include <memory> #include <iostream> #include <string> int main() { using namespace std::string_literals; std::vector<std::string> vs; std::find_if(vs.begin(), vs.end(), [](std::string const& s) { return s == "foo"s + "bar"s; }); std::find_if(vs.begin(), vs.end(), [p="foo"s + "bar"s](std::string const& s) { return s == p; }); }
捕获成员变量初始化程序也可以用于捕获成员变量。 然后,我们可以获得成员变量的副本,而不必担心悬挂链接。
例如:
struct Baz { auto foo() { return [s=s] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); }
您可以在此处使用代码:
@Wandbox
在
foo()
我们通过将成员变量复制到闭包类型来捕获它。 另外,我们使用auto输出整个方法(以前,在C ++ 11中,我们可以使用
std::function
)。
通用Lambda表达式另一个重要的改进是广义λ。
从C ++ 14开始,您可以编写:
auto foo = [](auto x) { std::cout << x << '\n'; }; foo(10); foo(10.1234); foo("hello world");
这等效于在闭包类型的调用语句中使用模板声明:
struct { template<typename T> void operator()(T x) const { std::cout << x << '\n'; } } someInstance;
当难以推断类型时,这种广义lambda可能非常有用。
例如:
std::map<std::string, int> numbers { { "one", 1 }, {"two", 2 }, { "three", 3 } };
我在这里错了吗? 输入的类型正确吗?
。
。
。
可能不是,因为std :: map的值类型是
std::pair<const Key, T>
。 因此,我的代码将制作这些行的其他副本...
这可以通过
auto
来解决:
std::for_each(std::begin(numbers), std::end(numbers), [](auto& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } );
您可以在此处使用代码:
@Wandbox结论真是个故事!
在本文中,我们从C ++ 03和C ++ 11中的lambda表达式开始的第一天开始,然后转到C ++ 14中的改进版本。
您已经了解了如何创建lambda,该表达式的基本结构是什么,捕获列表等等。
在本文的下一部分中,我们将继续学习C ++ 17,并了解C ++ 20的未来功能。
第二部分在这里可用:
Lambda:从C ++ 11到C ++ 20,第2部分
参考文献
C ++ 11-
[expr.prim.lambda]C ++ 14-
[expr.prim.lambda]C ++中的Lambda表达式| 微软文档揭秘C ++ lambdas-粘性位-由Feabhas提供支持;粘性位-由Feabhas提供支持
我们正在等待您的意见,并邀请对
“ C ++开发人员”课程感兴趣的所有人。