C++ lamda表达式 和 函数模版用法

文章目录

      • 前言
      • 函数对象
      • 匿名函数对象 lamda
        • 基本语法如下
        • lamda 的 优点
        • lamda的 参数捕获
        • 泛型 lamda 表达式
      • function 函数模版

前言

本节将对函数对象,匿名函数对象(lamda表达式) 以及 匿名函数对象和 函数模版结合对一些用法 做个笔记。同时,分析以下lamda表达式相比于传统函数对象的优劣。

关键是看一些 rocksdb类似的经典C++项目,老是有些细节get不到,导致最终理解的设计思想和实际的实现有差异。所以将这些高阶语法 再系统过一遍,加深对源码的理解。

函数对象

函数对象在C++98的时候已经比较成熟了,先看一段代码

struct adder {
    adder(int n) : n_(n) {}
    int operator() (int x)const {
        return x + n_;
    }
private:
    int n_;
};

这是一个函数对象的定义,通过adder能够实例化一个函数,auto add_3 = adder(3),那么add_3就可以作为一个+3的函数来使用。

auto add_3 = adder(3); // c++11 的初始化
adder add_3(3); // c++98的初始化
cout << " 5 add_3's result is " << add_3(5) << endl;

这里为什么add_3能够使用()括号的语法,就是因为adder类中定义了一个operator ()

同时C++98中也在定义了一些高介函数,来支持函数对象的创建。比较典型的有bind1stbind2nd (由头文件提供),基本用法如下:

auto add_3 = bind2nd(plus<int>(), 3); //c++11 的语法
binder2nd<plus<int>()>  add_2(plus<int>(), 3); // c++98的语法

将3作为的第二个参数绑定到函数对象plus, 返回的函数对象add_3同样拥有为每一个输入+3的功能。
如下代码,C++98中直接构造函数对象来为每个数组元素+3

// 为数组中的 每个元素 + 3
vector <int> arr = {1,2,3,4,5};
transform(arr.begin(), arr.end(),
          arr.begin(),
          bind2nd(plus<int>(),3));

匿名函数对象 lamda

基本语法如下

  • lamda 是以一对中括号 开头(中括号内是可以有内容的)
  • 和函数定义一样,需要拥有参数列表。紧跟在[] 之后的 int x
  • 和正常函数一样,有一个函数体,里面会有return 语句。
  • lamda表达式一般不需要说明返回值(默认是auto 类型的)
  • 每一个lamda表达式都由一个全局唯一的类型,想要精确得将 lamda表达式的返回值捕捉到,只能通过auto

如上add_3函数的功能可以 通过lamda写出如下的实现方法

auto add_3 = [](int x) {
	return x + 3;
};
cout << add_3(5) << endl;

同时如果想要实现一个通用的类似于上文中的addr类,则可以有如下实现

 auto adder = [](int x) {
     return [x](int n) {
         return x + n;
     };
 };
 
 cout << adder(3)(5) << endl; // 返回 3 + 5

以上代码通过x 来捕获变量x的数值,return的操作就是将 x + n的结果放到x中,然后被外层的x捕获。

lamda 的 优点

  • 立即求值。这样能够将独立的代码封装起来,简洁干净,明了。
  • 解决多重路径初始化问题,减少函数的拷贝和移动,提升性能。

第一个优点,之前的案例也能够体现,将加法功能函数进行封装,并能能够立即返回计算的数值。

auto res = [](int x) {
    return x * x;
}(9);
cout << res << endl; // 9的平方

第二优点,可以先看如下案例

Obj obj; // 默认构造函数
init_mode=1;
switch (init_mode)
{
case 1:
    obj = Obj(2); // 带参数的构造函数(我们真正想要调用的) + 赋值的构造函数
    break;
default:
    break;
}

我们想要根据输入,构造对应的obj对象,整个过程会调用Obj类的默认构造函数,带参数的构造函数,赋值构造函数。

那么以上代码可以通过lamda表达式来简化,并且不需要默认构造函数赋值构造函数的参与,仅仅完成参数构造即可返回。

auto obj_lamda = [init_mode]() {
    switch (init_mode)
    {
    case 1:
        return Obj(2);
        break;
   default:
        break;
    }
}();

完整测试代码如下:

#include 
#include 
#include 
#include 
#include 

using namespace std;

class Obj {
public:
    Obj(){cout << "default construct obj " << endl;}
    Obj(int x) : x_(x) {cout << "paramter construct obj" << endl;}
    Obj(Obj &&obj) {
        cout << "move construct obj" << endl;
        this->x_ = obj.x_;
    }
    Obj& operator=(const Obj &obj) {
        this->x_ = obj.x_;
        cout << "assign construct obj " << endl;
        return *this;
    }
    
    ~Obj() {}

private:
    int x_;
};
int main() {

    int init_mode = 1;
    cout << "ordinary construct : " << endl;
    Obj obj;
    switch (init_mode)
    {
    case 1:
        obj = Obj(2);
        break;
   default:
        break;
    }

    cout << "lamda construct : " << endl;
    auto obj_lamda = [init_mode]() {
        switch (init_mode)
        {
        case 1:
            return Obj(2);
            break;
       default:
            break;
        }
    }();
    
	return 0;
}

输出如下:

ordinary construct : 
default construct obj 
paramter construct obj
assign construct obj 

lamda construct : 
paramter construct obj

lamda的 参数捕获

以上的lamda表达式的案例中,lamda后面的[]中传入参数,可以在其后{}的函数体中捕获。
接下来看一下捕获过程的一些细节:
变量捕获的开头是可选的捕获符 =&, 其中=是默认的捕获符。 这个捕获的过程是自动按值= 或引用&捕获用到的本地变量,后面可以跟,进行分隔。

  • 本定变量名表明对其按值捕获(不能在默认捕获符 = 后出现;因其已自动按值捕获所有本地变量)
  • &加本地变量名,标明对其按引用捕获(不能在默认捕获符&后出现,因其已自动按引用捕获)
  • this 标明按引用捕获外围对象(主要是针对lamda表达式定义出现在一个非静态类成员内部的情况),注意默认=和&会自动捕获this对象
  • *this 标明按值捕获外围对象(同样针对lamda 表达式定义出现在一个非静态类成员内部的情况)
  • 变量名 = 表达式 标明按值捕获表达式的结果(可理解为 auto var = expression)
  • &变量名 = 表达式 标明按引用捕获表达式的结果(可理解为 auto &var = expression)

这里需要注意,一般在使用按引用捕获 某个变量的时候需要有下面的需求:

  1. 需要在lamda 表达式中修改这个变量,并且这个变量要能够让外部观察到
  2. 需要看到这个变量在外部被修改的结果
  3. 这个变量的复制代价比较高

举例1: 按引用捕获变量
按引用捕获变量 v1,v2,并修改其值。

vector <int> v1;
vector <int> v2;
// ...

auto push_data = [&](int x) {
	//这里也可以使用 [&v1,&v2]进行捕获
	v1.push_back(x);
	v2.push_back(y);
};

push_data(2);
push_data(3);

举例2: 按值捕获变量
查看如下代码,使用多个线程进行各自对象的复制,从而支持独立运算。

#include 
#include 
#include 
#include 

using namespace std;

int get_count() {
    static int count = 0;
    return ++count;
}

class Task {
public:
    Task(int data): data_(data) {}
    auto lazy_lunch() {
        return
            [*this, count = get_count()] () // 按值捕获外部对象
            mutable { //mutable 标记捕获的内容可以更改
                ostringstream oss;
                oss << "Done work " << data_
                    << " (No. " << count
                    << ") in thread " 
                    << this_thread::get_id() 
                    << "\n";
                msg_ = oss.str(); //更改来msg_的值
                caculate();
            };
    }

    void caculate() {
        this_thread::sleep_for(100ms);
        cout << msg_;
    }

private:
    int data_;
    string msg_;
};


int main() {

    auto t = Task{11};
    thread t1 {t.lazy_lunch()};
    thread t2 {t.lazy_lunch()};

    t1.join();
    t2.join();
	
	return 0;
}

输出如下:

#捕获了this的引用,即this的修改可以被外部观察。
#两个不同的线程对象修改了各自的msg_的值,所以打印的msg_的地址内容 不同
Done work 11 (No. 1) in thread 0x70000def5000
Done work 11 (No. 2) in thread 0x70000df78000

注意这段代码需要使用c++14的标准进行编译,因为lamda 表达式中的*thiscount = get_count()都是14标准中的特性。

以上代码使用了lamda表达式的几个特性:

  • mutable标记捕获的内容可以被更改
  • [*this] 表示按值捕获外围对象(Task)
  • [count = get_count()] 捕获表达式可以生成lamda表达式时计算并存储等号后的表达式结果。即将get_count函数执行完,并将返回的结果赋值给count保存下来。

如果以上代码我们 针对this 按值捕获[this],则不会更改msg_的地址

#捕获了this的值,即使不同的线程对象在内部更改了msg_的内容,也不会被外部观察到。
# 所以打印的 msg_ 的地址相同,也即Task 初始化时msg_的地址
Done work 11 (No. 2) in thread 0x700008cf5000
Done work 11 (No. 2) in thread 0x700008cf5000

泛型 lamda 表达式

函数的返回值可以是auto,但是参数需要一一声明。
这个过程在lamda表达式中进一步简化,声明参数是可以直接使用auto(包括auto &&),整体也就是类似于自己声明了模版。主要还是因为lamda表达式的参数中无法使用tmplate关键字。

如下案例

template <typename T, typename V>
auto sum(T t, V v) {
	return t + v;
}

跟上面函数等价的lamda表达式是:

auto sum = [](auto t, auto v) {
	return x + v;
};

为什么要推出lamda表达式的泛型,还是为了组合性的提升,以上lamda表达式 案例中的sum 就类似于标准库中plus函数模版。函数对象可以 传递给其他接收函数对象的函数,而 + 则无法完成这个操作。

#include 
#include 
#includ 

using namespace std;

int main(){
    std::array<int,5> a{1,2,3,4,5};
    auto s = accumulate( // 这个数学函数是进行累加和的操作
       a.begin(), a.end(), 1,
       [](auto x, auto y) {
           return x + y;
       }
    );

    cout << s << endl; 
    return 0;
}

如以上案例,使用lamda表达式构造 作为 累加和的数学函数accumulate的一个参数,完成所有传入数值的相加。
但是当lamda的行为发生变化,将return x + y改为return x *y,则就变成了计算5的阶乘的操作,即使这个函数是进行累加和的运算。

function 函数模版

之前说过,lamda表达式的语法中每一个lamda表达式都由一个全局唯一的类型,所以只能用auto 或 模版参数来接收结果。但很多时候需要这个接收的过程更为通用,所以需要function模版。
function 模版的参数就是函数的类型,一个函数对象放到fucntion里之后,外界只能观察到其参数、返回值类型和执行结果。

ps :function模版的创建非常消耗资源,所以如果auto解决不了的时候再使用函数模版

举例如下:
这个例子中,需要将函数对象放到一个map中,所以需要使用function 函数模版来完成。

std::map<string ,function<int(int, int)>> 
    op_dict{
        {"+",
            [](int x, int y) {
                return x + y;
            }
        },
        {"-",
            [](int x, int y) {
                return x - y;
            }
        },
        {"*",
            [](int x, int y) {
                return x * y;
            }
        },
        { "/",
            [](int x ,int y) {
                assert(y!=0);
                return x / y;
            }
        }
    };

最终可以通过调用op_dict["+"]\(1,2)的方式 ,完成1 + 2 的计算,这种方式一般对表达式对解析会比较有用。

你可能感兴趣的:(#,编程语言:C++,编程语言)