专业编程基础技术教程

网站首页 > 基础教程 正文

C++17 lambda表达式详解 c语言lambda表达式

ccvgpt 2024-10-12 13:51:33 基础教程 7 ℃

Lambda 表达式

Lambda 表达式是现代 C++ 中最重要的特性之一,而 Lambda 表达式,实际上就是提供了一个类似匿名函数的特性, 而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。这样的场景其实有很多很多, 所以匿名函数几乎是现代编程语言的标配。

基础

Lambda 表达式的基本语法如下:

C++17 lambda表达式详解 c语言lambda表达式

 [捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
 // 函数体
 }
 void lambda_value_capture() {
     int value = 1;
     auto copy_value = [value] {
         return value;
     };
     value = 100;
     auto stored_value = copy_value();
     std::cout << "stored_value = " << stored_value << std::endl;
     // 这时, stored_value == 1, 而 value == 100.
     // 因为 copy_value 在创建时就保存了一份 value 的拷贝
 }
 void lambda_reference_capture() {
     int value = 1;
     auto copy_value = [&value] {
         return value;
     };
     value = 100;
     auto stored_value = copy_value();
     std::cout << "stored_value = " << stored_value << std::endl;
     // 这时, stored_value == 100, value == 100.
     // 因为 copy_value 保存的是引用
 }
 #include <iostream>
 #include <utility>
 
 int main() {
     auto important = std::make_unique<int>(1);
     auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
         return x+y+v1+(*v2);
     };
     std::cout << add(3,4) << std::endl;
     return 0;
 }
 auto add = [](auto x, auto y) {
     return x+y;
 };
 ?
 add(1, 2);
 add(1.1, 2.2);
std::function #include <iostream>
 ?
 using foo = void(int); // 定义函数类型, using 的使用见上一节中的别名语法
 void functional(foo f) { // 定义在参数列表中的函数类型 foo 被视为退化后的函数指针类型 foo*
     f(1); // 通过函数指针调用函数
 }
 ?
 int main() {
     auto f = [](int value) {
         std::cout << value << std::endl;
    };
     functional(f); // 传递闭包对象,隐式转换为 foo* 类型的函数指针值
     f(1); // lambda 表达式调用
     return 0;
 } #include <functional>
 #include <iostream>
 ?
 int foo(int para) {
     return para;
 }
 ?
 int main() {
     // std::function 包装了一个返回值为 int, 参数为 int 的函数
     std::function<int(int)> func = foo;
 ?
     int important = 10;
     std::function<int(int)> func2 = [&](int value) -> int {
         return 1+value+important;
    };
     std::cout << func(10) << std::endl;
     std::cout << func2(10) << std::endl;
 }std::bind 和 std::placeholder int foo(int a, int b, int c) {
    ;
 }
 int main() {
     // 将参数1,2绑定到函数 foo 上,但是使用 std::placeholders::_1 来对第一个参数进行占位
     auto bindFoo = std::bind(foo, std::placeholders::_1, 1,2);
     // 这时调用 bindFoo 时,只需要提供第一个参数即可
     bindFoo(1);
 }许可进一步阅读的参考文献总结template<typename _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type& __t) noexcept { return static_cast<_Tp&&>(__t); } template<typename _Tp> constexpr _Tp&& forward(typename std::remove_reference<_Tp>::type&& __t) noexcept { static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument" " substituting _Tp is an lvalue reference type"); return static_cast<_Tp&&>(__t); }传递右值: 普通传参: 左值引用 std::move 传参: 右值引用 std::forward 传参: 右值引用 static_cast<T&&> 传参: 右值引用 传递左值: 普通传参: 左值引用 std::move 传参: 右值引用 std::forward 传参: 左值引用 static_cast<T&&> 传参: 左值引用#include <iostream> #include <utility> void reference(int& v) { std::cout << "左值引用" << std::endl; } void reference(int&& v) { std::cout << "右值引用" << std::endl; } template <typename T> void pass(T&& v) { std::cout << " 普通传参: "; reference(v); std::cout << " std::move 传参: "; reference(std::move(v)); std::cout << " std::forward 传参: "; reference(std::forward<T>(v)); std::cout << "static_cast<T&&> 传参: "; reference(static_cast<T&&>(v)); } int main() { std::cout << "传递右值:" << std::endl; pass(1); std::cout << "传递左值:" << std::endl; int v = 1; pass(v); return 0; }函数形参类型实参参数类型推导后函数形参类型T&左引用T&T&右引用T&T&&左引用T&T&&右引用T&&void reference(int& v) { std::cout << "左值" << std::endl; } void reference(int&& v) { std::cout << "右值" << std::endl; } template <typename T> void pass(T&& v) { std::cout << "普通传参:"; reference(v); // 始终调用 reference(int&) } int main() { std::cout << "传递右值:" << std::endl; pass(1); // 1是右值, 但输出是左值 std::cout << "传递左值:" << std::endl; int l = 1; pass(l); // l 是左值, 输出左值 return 0; }完美转发#include <iostream> // std::cout #include <utility> // std::move #include <vector> // std::vector #include <string> // std::string int main() { std::string str = "Hello world."; std::vector<std::string> v; // 将使用 push_back(const T&), 即产生拷贝行为 v.push_back(str); // 将输出 "str: Hello world." std::cout << "str: " << str << std::endl; // 将使用 push_back(const T&&), 不会出现拷贝行为 // 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销 // 这步操作后, str 中的值会变为空 v.push_back(std::move(str)); // 将输出 "str: " std::cout << "str: " << str << std::endl; return 0; }#include <iostream> class A { public: int *pointer; A():pointer(new int(1)) { std::cout << "构造" << pointer << std::endl; } A(A& a):pointer(new int(*a.pointer)) { std::cout << "拷贝" << pointer << std::endl; } // 无意义的对象拷贝 A(A&& a):pointer(a.pointer) { a.pointer = nullptr; std::cout << "移动" << pointer << std::endl; } ~A(){ std::cout << "析构" << pointer << std::endl; delete pointer; } }; // 防止编译器优化 A return_rvalue(bool test) { A a,b; if(test) return a; // 等价于 static_cast<A&&>(a); else return b; // 等价于 static_cast<A&&>(b); } int main() { A obj = return_rvalue(false); std::cout << "obj:" << std::endl; std::cout << obj.pointer << std::endl; std::cout << *obj.pointer << std::endl; return 0; }移动语义void increase(int & v) { v++; } void foo() { double s = 1; increase(s); }#include <iostream> int main() { // int &a = std::move(1); // 不合法,非常量左引用无法引用右值 const int &b = std::move(1); // 合法, 常量左引用允许引用右值 std::cout << a << b << std::endl; }#include <iostream> #include <string> void reference(std::string& str) { std::cout << "左值" << std::endl; } void reference(std::string&& str) { std::cout << "右值" << std::endl; } int main() { std::string lv1 = "string,"; // lv1 是一个左值 // std::string&& r1 = lv1; // 非法, 右值引用不能引用左值 std::string&& rv1 = std::move(lv1); // 合法, std::move可以将左值转移为右值 std::cout << rv1 << std::endl; // string, const std::string& lv2 = lv1 + lv1; // 合法, 常量左值引用能够延长临时变量的生命周期 // lv2 += "Test"; // 非法, 常量引用无法被修改 std::cout << lv2 << std::endl; // string,string std::string&& rv2 = lv1 + lv2; // 合法, 右值引用延长临时对象生命周期 rv2 += "Test"; // 合法, 非常量引用能够修改临时变量 std::cout << rv2 << std::endl; // string,string,string,Test reference(rv2); // 输出左值 return 0; }右值引用和左值引用std::vector<int> foo() { std::vector<int> temp = {1, 2, 3, 4}; return temp; } std::vector<int> v = foo();class Foo { const char*&& right = "this is a rvalue"; // 此处字符串字面量为右值 public: void bar() { right = "still rvalue"; // 此处字符串字面量为右值 } }; 
 int main() { const char* const &left = "this is an lvalue"; // 此处字符串字面量为左值 }左值、右值的纯右值、将亡值、右值3.3 右值引用

这部分内容虽然属于标准库的一部分,但是从本质上来看,它却增强了 C++ 语言运行时的能力, 这部分内容也相当重要,所以放到这里来进行介绍。

函数对象包装器

幸运的是,这种麻烦只存在于 C++11 中,从 C++14 开始, Lambda 函数的形式参数可以使用 auto 关键字来产生意义上的泛型:

上一节中我们提到了 auto 关键字不能够用在参数表里,这是因为这样的写法会与模板的功能产生冲突。 但是 Lambda 表达式并不是普通函数,所以 Lambda 表达式并不能够模板化。 这就为我们造成了一定程度上的麻烦:参数表不能够泛化,必须明确参数表类型。

泛型 Lambda

在上面的代码中,important 是一个独占指针,是不能够被捕获到的,这时候我们需要将其转移为右值, 在表达式中初始化。

C++14 给与了我们方便,允许捕获的成员用任意的表达式进行初始化,这就允许了右值的捕获, 被声明的捕获变量类型会根据表达式进行判断,判断方式与使用 auto 本质上是相同的:

上面提到的值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值,而不能捕获右值。

这部分内容需要了解后面马上要提到的右值引用以及智能指针

4. 表达式捕获

  • [] 空捕获列表
  • [name1, name2, ...] 捕获一系列变量
  • [&] 引用捕获, 让编译器自行推导捕获列表
  • [=] 值捕获, 让编译器执行推导引用列表

总结一下,捕获提供了lambda 表达式对外部值进行使用的功能,捕获列表的最常用的四种形式可以是:

手动书写捕获列表有时候是非常复杂的,这种机械性的工作可以交给编译器来处理,这时候可以在捕获列表中写一个 & 或 = 向编译器声明采用引用捕获或者值捕获.

3. 隐式捕获


与引用传参类似,引用捕获保存的是引用,值会发生变化。

引用捕获

与参数传值类似,值捕获的前提是变量可以拷贝,不同之处则在于,被捕获的变量在 lambda 表达式被创建时拷贝, 而非调用时才拷贝:

1. 值捕获

所谓捕获列表,其实可以理解为参数的一种类型,lambda 表达式内部函数体在默认情况下是不能够使用函数体外部的变量的, 这时候捕获列表可以起到传递外部数据的作用。根据传递的行为,捕获列表也分为以下几种:

上面的语法规则除了 [捕获列表] 内的东西外,其他部分都很好理解,只是一般函数的函数名被略去, 返回值使用了一个 -> 的形式进行(我们在上一节前面的尾返回类型已经提到过这种写法了)。

移动语义

传统 C++ 通过拷贝构造函数和赋值操作符为类对象设计了拷贝/复制的概念,但为了实现对资源的移动操作, 调用者必须使用先复制、再析构的方式,否则就需要自己实现移动对象的接口。 试想,搬家的时候是把家里的东西直接搬到新家去,而不是将所有东西复制一份(重买)再放到新家、 再把原来的东西全部扔掉(销毁),这是非常反人类的一件事情。

传统的 C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据拷贝,浪费时间和空间。 右值引用的出现恰好就解决了这两个概念的混淆问题,例如:

 #include <iostream>
 class A {
 public:
     int *pointer;
     A():pointer(new int(1)) { 
         std::cout << "构造" << pointer << std::endl; 
     }
     A(A& a):pointer(new int(*a.pointer)) { 
         std::cout << "拷贝" << pointer << std::endl; 
     } // 无意义的对象拷贝
     A(A&& a):pointer(a.pointer) { 
         a.pointer = nullptr;
         std::cout << "移动" << pointer << std::endl; 
     }
     ~A(){ 
         std::cout << "析构" << pointer << std::endl; 
         delete pointer; 
     }
 };
 // 防止编译器优化
 A return_rvalue(bool test) {
     A a,b;
     if(test) return a; // 等价于 static_cast<A&&>(a);
     else return b;     // 等价于 static_cast<A&&>(b);
 }
 int main() {
     A obj = return_rvalue(false);
     std::cout << "obj:" << std::endl;
     std::cout << obj.pointer << std::endl;
     std::cout << *obj.pointer << std::endl;
     return 0;
 }
 #include <iostream> // std::cout
 #include <utility> // std::move
 #include <vector> // std::vector
 #include <string> // std::string
 ?
 int main() {
 ?
 std::string str = "Hello world.";
 std::vector<std::string> v;
 ?
     // 将使用 push_back(const T&), 即产生拷贝行为
     v.push_back(str);
     // 将输出 "str: Hello world."
     std::cout << "str: " << str << std::endl;
 ?
     // 将使用 push_back(const T&&), 不会出现拷贝行为
     // 而整个字符串会被移动到 vector 中,所以有时候 std::move 会用来减少拷贝出现的开销
     // 这步操作后, str 中的值会变为空
     v.push_back(std::move(str));
     // 将输出 "str: "
     std::cout << "str: " << str << std::endl;
 ?
     return 0;
 }
 void reference(int& v) {
     std::cout << "左值" << std::endl;
 }
 void reference(int&& v) {
     std::cout << "右值" << std::endl;
 }
 template <typename T>
 void pass(T&& v) {
     std::cout << "普通传参:";
     reference(v); // 始终调用 reference(int&)
 }
 int main() {
     std::cout << "传递右值:" << std::endl;
     pass(1); // 1是右值, 但输出是左值
 ?
     std::cout << "传递左值:" << std::endl;
     int l = 1;
     pass(l); // l 是左值, 输出左值
 ?
     return 0;
 }
 #include <iostream>
 #include <utility>
 void reference(int& v) {
     std::cout << "左值引用" << std::endl;
 }
 void reference(int&& v) {
     std::cout << "右值引用" << std::endl;
 }
 template <typename T>
 void pass(T&& v) {
     std::cout << "              普通传参: ";
     reference(v);
     std::cout << "       std::move 传参: ";
     reference(std::move(v));
     std::cout << "    std::forward 传参: ";
     reference(std::forward<T>(v));
     std::cout << "static_cast<T&&> 传参: ";
     reference(static_cast<T&&>(v));
 }
 int main() {
     std::cout << "传递右值:" << std::endl;
     pass(1);
 ?
     std::cout << "传递左值:" << std::endl;
     int v = 1;
     pass(v);
 ?
     return 0;
 }
 传递右值:
               普通传参: 左值引用
        std::move 传参: 右值引用
     std::forward 传参: 右值引用
 static_cast<T&&> 传参: 右值引用
 传递左值:
               普通传参: 左值引用
        std::move 传参: 右值引用
     std::forward 传参: 左值引用
 static_cast<T&&> 传参: 左值引用

无论传递参数为左值还是右值,普通传参都会将参数作为左值进行转发, 所以 std::move 总会接受到一个左值,从而转发调用了reference(int&&) 输出右值引用。

输出结果为:

完美转发就是基于上述规律产生的。所谓完美转发,就是为了让我们在传递参数的时候, 保持原来的参数类型(左引用保持左引用,右引用保持右引用)。 为了解决这个问题,我们应该使用 std::forward 来进行参数的转发(传递):

因此,模板函数中使用 T&& 不一定能进行右值引用,当传入左值时,此函数的引用将被推导为左值。 更准确的讲,无论模板参数是什么类型的引用,当且仅当实参类型为右引用时,模板参数才能被推导为右引用类型。 这才使得 v 作为左值的成功传递。

函数形参类型实参参数类型推导后函数形参类型T&左引用T&T&右引用T&T&&左引用T&T&&右引用T&&

这是基于引用坍缩规则的:在传统 C++ 中,我们不能够对一个引用类型继续进行引用, 但 C++ 由于右值引用的出现而放宽了这一做法,从而产生了引用坍缩规则,允许我们对引用进行引用, 既能左引用,又能右引用。但是却遵循如下规则:

对于 pass(1) 来说,虽然传递的是右值,但由于 v 是一个引用,所以同时也是左值。 因此 reference(v) 会调用 reference(int&),输出『左值』。 而对于pass(l)而言,l是一个左值,为什么会成功传递给 pass(T&&) 呢?

前面我们提到了,一个声明的右值引用其实是一个左值。这就为我们进行参数转发(传递)造成了问题:

完美转发

从而避免了无意义的拷贝构造,加强了性能。再来看看涉及标准库的例子:

  1. 首先会在 return_rvalue 内部构造两个 A 对象,于是获得两个构造函数的输出;
  2. 函数返回后,产生一个将亡值,被 A 的移动构造(A(A&&))引用,从而延长生命周期,并将这个右值中的指针拿到,保存到了 obj 中,而将亡值的指针被设置为 nullptr,防止了这块内存区域被销毁。

在上面的代码中:

最近发表
标签列表