Lambda:从C ++ 11到C ++ 20。 第一部分

朋友们,下午好。 今天,我们为您准备了文章“ 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; }(); // <-- call () std::cout << x << " " << y << std::endl; 

这样的表达式在常量对象的复杂初始化中很有用。

 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; }); } 

您可以在此处使用代码: @Wandbox

C ++ 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 } }; //      pair<const string, int>! std::for_each(std::begin(numbers), std::end(numbers), [](const std::pair<std::string, int>& entry) { std::cout << entry.first << " = " << entry.second << '\n'; } ); 

我在这里错了吗? 输入的类型正确吗?



可能不是,因为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 ++开发人员”课程感兴趣的所有人。

Source: https://habr.com/ru/post/zh-CN444524/


All Articles