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

嗨哈布罗夫斯克 关于开始在“ C ++开发人员”课程中招募新成员的事宜 ,我们与您分享文章“ Lambdas:从C ++ 11到C ++ 20”的第二部分的翻译。 第一部分可以在这里阅读。



在本系列第一部分中,我们从C ++ 03,C ++ 11和C ++ 14的角度看了lambda。 在本文中,我描述了此强大的C ++功能背后的动机,基本用法,语法以及每种语言标准中的改进。 我还提到了一些临界情况。
现在是时候进入C ++ 17并展望未来(非常接近!):C ++ 20。

参赛作品

快速提醒一下:本系列的想法是在我们最近在克拉科夫举行的C ++用户组会议之后提出的。

我们进行了有关lambda表达式“历史”的现场编程会议。 对话是由C ++专家Thomas Kaminsky主持的( 请参阅Thomas的Linkedin个人资料 )。 这是事件:
Lambda:从C ++ 11到C ++ 20-C ++用户组克拉科夫

我决定从Thomas那里获得代码(在他的允许下!),并以此为基础编写文章在本系列的第一部分中,我谈到了lambda表达式,如下所示:

  • 基本语法
  • λ型
  • 呼叫接线员
  • 捕获变量(可变,全局,静态变量,类成员和此指针,仅可移动对象,存储常量):

    • 返回类型
    • IIFE-立即调用函数表达式
    • 转换为函数指针
    • 返回类型
    • IIFE-立即调用的表达式
    • 转换为函数指针
  • C ++ 14的改进

    • 返回类型输出
    • 用初始化器捕获
    • 捕获成员变量
    • 通用Lambda表达式

上面的列表只是lambda表达式历史的一部分!

现在,让我们看看C ++ 17中发生了什么变化以及C ++ 20中有什么变化!

C ++ 17的改进

关于Lambda的标准(发布前起草) N659部分: [expr.prim.lambda] 。 C ++ 17对lambda表达式进行了两项重大改进:

  • constexpr lambda
  • 捕捉*这个

这些创新对我们意味着什么? 让我们弄清楚。

constexpr lambda表达式

从C ++ 17开始,如果可能,该标准将lambda类型的operator()隐式定义为constexpr
来自expr.prim.lambda#4
如果相应的lambda表达式的条件参数的声明后跟constexpr,或者它满足constexpr函数的要求,则函数调用运算符是constexpr函数。

例如:

 constexpr auto Square = [] (int n) { return n*n; }; // implicitly constexpr static_assert(Square(2) == 4); 

回想一下,在C ++ 17 constexpr函数必须遵循以下规则:

  • 它不应该是虚拟的;

    • 它的返回类型必须是文字类型;
    • 其参数的每种类型都必须是文字类型;
    • 它的主体必须是= delete,= default或不包含的复合语句
      • asm定义
      • goto表达式,
      • 标签
      • 尝试阻止或
      • 不执行初始化的非文字变量,静态变量或流存储变量的定义。

那一个更实际的例子呢?

 template<typename Range, typename Func, typename T> constexpr T SimpleAccumulate(const Range& range, Func func, T init) { for (auto &&elem: range) { init += func(elem); } return init; } int main() { constexpr std::array arr{ 1, 2, 3 }; static_assert(SimpleAccumulate(arr, [](int i) { return i * i; }, 0) == 14); } 

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

该代码使用constexpr lambda,然后将其传递给简单的SimpleAccumulate算法。 该算法使用了几个C ++ 17元素: constexpr添加到std::arraystd::beginstd::end (在带范围的for循环中使用)现在也都是constexpr ,因此这意味着可以执行所有代码在编译时。

当然,这还不是全部。

您可以捕获变量(前提是它们也为constexpr ):

 constexpr int add(int const& t, int const& u) { return t + u; } int main() { constexpr int x = 0; constexpr auto lam = [x](int n) { return add(x, n); }; static_assert(lam(10) == 10); } 

但是有一种有趣的情况,就是您不进一步传递捕获的变量,例如:

 constexpr int x = 0; constexpr auto lam = [x](int n) { return n + x }; 

在这种情况下,在Clang中我们会收到以下警告:

warning: lambda capture 'x' is not required to be captured for this use

这可能是由于x在每次使用时都可以更改的事实(除非您进一步转让x或使用此名称的地址)。

但是,请告诉我您是否知道此行为的官方规则。 我仅从cppreference找到了(但在草案中找不到...)

(译者注:正如我们的读者所写,我可能是想在每个使用“ x”的地方替换它的值。绝对不可能更改它。)

Lambda表达式可以读取变量的值,而无需捕获该变量的值
*具有常量non-volatile整数或枚举类型,并已使用constexpr
*是constexpr ,没有可变成员。

为未来做好准备:

在C ++ 20中,我们将拥有constexpr标准算法,甚至可能还有一些容器,因此constexpr lambda在这种情况下将非常有用。 对于运行时版本和constexpr版本(编译时版本),您的代码看起来都一样!

简而言之:

constexpr lambda允许您与样板编程保持一致,并且代码可能更短。

现在让我们继续了解C ++ 17中可用的第二个重要功能:

*这里的捕获
捕捉*这个

当我们想吸引班级成员时,您还记得我们的问题吗? 默认情况下,我们捕获此对象(作为指针!),因此当临时对象超出范围时,我们可能会遇到问题...这可以使用带有初始化程序的capture方法来解决(请参阅本系列的第一部分)。 但是现在,在C ++ 17中,我们有了另一种方式。 我们可以包装*这样的副本:

 #include <iostream> struct Baz { auto foo() { return [*this] { std::cout << s << std::endl; }; } std::string s; }; int main() { auto f1 = Baz{"ala"}.foo(); auto f2 = Baz{"ula"}.foo(); f1(); f2(); } 

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

使用带有初始值设定项的捕获来捕获所需的成员变量可以保护您避免出现带有临时值的错误,但是当我们要调用类似这样的方法时,我们不能做同样的事情:

例如:

 struct Baz { auto foo() { return [this] { print(); }; } void print() const { std::cout << s << '\n'; } std::string s; }; 

在C ++ 14中,使代码更安全的唯一方法是使用初始化程序捕获this

 auto foo() { return [self=*this] { self.print(); }; }   C ++ 17    : auto foo() { return [*this] { print(); }; } 

还有一件事:

请注意,如果您在成员函数中编写[=] ,则将隐式捕获它! 这可能会在将来导致错误...并且它将在C ++ 20中过时。

因此,我们进入下一部分:未来。

C ++ 20的未来

在C ++ 20中,我们获得以下功能:

  • 允许[=, this]作为lambda捕获-P0409R2,并通过[=] - P0806取消对此的隐式捕获
  • lambda init-capture: ... args = std::move (args)] () {}包扩展lambda init-capture: ... args = std::move (args)] () {} - P0780
  • 用于结构化绑定的静态, thread_local和lambda捕获thread_local
  • λ模式(也带有概念) -P0428R2
  • 简化隐式Lambda捕获-P0588R1
  • 可构造且可分配的 Lambda,但不保存默认状态-P0624R2
  • 计算上下文中的Lambda- P0315R4

在大多数情况下,新引入的功能“清除”了lambda的使用,并且允许某些高级用例。

例如,使用P1091,您可以捕获结构化绑定。

我们也有与此相关的说明。 在C ++ 20中,如果在方法中捕获[=] ,则会收到警告:

 struct Baz { auto foo() { return [=] { std::cout << s << std::endl; }; } std::string s; }; GCC 9: warning: implicit capture of 'this' via '[=]' is deprecated in C++20 

如果确实需要捕获此内容,则应编写[=, this]

还存在与高级用例相关的更改,例如可以默认构造的无状态上下文和无状态lambda。

通过这两项更改,您可以编写:

 std::map<int, int, decltype([](int x, int y) { return x > y; })> map; 

在句子的第一个版本中阅读这些功能的动机: P0315R0P0624R0

但是,让我们看一下一个有趣的功能:lambda模板。

羔羊图案

在C ++ 14中,我们得到了广义的lambda,这意味着声明为auto的参数是模板参数。

对于lambda:

 [](auto x) { x; } 

编译器生成与以下样板方法匹配的调用语句:

 template<typename T> void operator(T x) { x; } 

但是无法更改此模板参数并使用实际的模板参数。 在C ++ 20中,这是可能的。

例如,如何限制lambda仅适用于某种类型的向量?

我们可以写一个一般的lambda:

 auto foo = []<typename T>(const auto& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

但是,如果使用int参数调用它(例如foo(10); ),则可能会遇到一些难以理解的错误:

 prog.cc: In instantiation of 'main()::<lambda(const auto:1&)> [with auto:1 = int]': prog.cc:16:11: required from here prog.cc:11:30: error: no matching function for call to 'size(const int&)' 11 | std::cout<< std::size(vec) << '\n'; 

在C ++ 20中,我们可以编写:

 auto foo = []<typename T>(std::vector<T> const& vec) { std::cout<< std::size(vec) << '\n'; std::cout<< vec.capacity() << '\n'; }; 

上面的lambda允许模板调用语句:

 <typename T> void operator(std::vector<T> const& s) { ... } 

template参数在capture子句[]

如果使用int (foo(10);)调用它,则会得到更好的消息:

 note: mismatched types 'const std::vector<T>' and 'int' 


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

在上面的示例中,编译器可以警告我们有关lambda接口而不是主体内部代码中的不一致之处。

另一个重要方面是,在通用lambda中,您只有变量,而没有模板类型。 因此,如果要访问它,则必须使用decltype(x)(对于带有参数(auto x)的lambda表达式)。 这使某些代码更加冗长和复杂。

例如(使用来自P0428的代码):

 auto f = [](auto const& x) { using T = std::decay_t<decltype(x)>; T copy = x; T::static_function(); using Iterator = typename T::iterator; } 

现在您可以这样写:

 auto f = []<typename T>(T const& x) { T::static_function(); T copy = x; using Iterator = typename T::iterator; } 

在上面的部分中,我们对C ++ 20进行了简要概述,但是我还有另外一个用例。 在C ++ 14中甚至可以使用此技术。 继续阅读。

奖金-使用Lambdas提升

当前,当您有函数重载并且想要将其传递给标准算法(或任何需要一些称为对象的东西)时,我们会遇到一个问题:

 // two overloads: void foo(int) {} void foo(float) {} int main() { std::vector<int> vi; std::for_each(vi.begin(), vi.end(), foo); } 

我们从GCC 9(货车)中收到以下错误:

 error: no matching function for call to for_each(std::vector<int>::iterator, std::vector<int>::iterator, <unresolved overloaded function type>) std::for_each(vi.begin(), vi.end(), foo); ^^^^^ 

但是,有一个技巧,我们可以使用lambda然后调用所需的重载函数。

以基本形式,对于简单的值类型,对于我们的两个函数,我们可以编写以下代码:

 std::for_each(vi.begin(), vi.end(), [](auto x) { return foo(x); }); 

以最一般的形式,我们需要再输入一些:

 #define LIFT(foo) \ [](auto&&... x) \ noexcept(noexcept(foo(std::forward<decltype(x)>(x)...))) \ -> decltype(foo(std::forward<decltype(x)>(x)...)) \ { return foo(std::forward<decltype(x)>(x)...); } 

非常复杂的代码...对吗? :)

让我们尝试解密它:

我们创建一个通用的lambda,然后传递我们得到的所有参数。 为了正确地确定它,我们需要指定noexcept和返回值的类型。 这就是为什么我们必须复制调用代码-为了获得正确的类型。
这样的LIFT宏可在任何支持C ++ 14的编译器中使用。

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

结论

在本文中,我们研究了C ++ 17中的重大变化,并概述了C ++ 20中的新功能。

您可能会注意到,随着语言的每次迭代,lambda表达式会与其他C ++元素混合在一起。 例如,在C ++ 17之前,我们不能在constexpr的上下文中使用它们,但是现在有可能。 与以C ++ 14开始的通用lambda相似,它们以模板lambda的形式演变为C ++ 20。 我想念什么吗? 也许您有一些令人兴奋的例子? 请在评论中让我知道!

参考文献

C ++ 11- [expr.prim.lambda]
C ++ 14- [expr.prim.lambda]
C ++ 17- [expr.prim.lambda]
C ++中的Lambda表达式| 微软文档
Simon Brand-将重载集传递给函数
Jason Turner- C ++每周-剧集128-Lambda的C ++ 20模板语法
Jason Turner- C ++每周-剧集41-C ++ 17的constexpr Lambda支持

我们邀请所有人参加将于6月14日举行的传统免费网络研讨会

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


All Articles