美文网首页
C++ 11(3) ---- 函数对象包装器和lamda 表达式

C++ 11(3) ---- 函数对象包装器和lamda 表达式

作者: 特立独行的佩奇 | 来源:发表于2023-03-23 21:04 被阅读0次

函数对象的引入

C 中已经存在函数指针的概念,函数指针指向的是内存中的一段代码而非一段可以读写的数据,函数指针最常用的形式就是回调函数,回调函数的本质就是在A模块中定义,在B模块中被调用,如下图:


callback.jpg

因为在A模块定义,因此可以使用A模块变量的作用域,在B模块中被调用,所以调用的实参是从B模块传入的,但是我们有时候有这样的情形,我们依然需要在模块A定义函数,同时函数A的运行需要依赖B模块产生的数据,然后将模块A定义的函数和模块B产生的数据一并传递给C模块来调用,如下图所示:


special_case.jpg

我们依然需要在模块A定义函数,同时函数A的运行需要依赖B模块产生的数据,然后将模块A定义的函数和模块B产生的数据一并传递给C模块来调用,就像这样:

typedef void (*func) (int);

struct closure{
  funcpointer f;
  int arg;    
};

此时可以定义一个结构体,将函数指针和数据打包起来

void run(struct closure func) {
    func->f(func->arg);
}

closure既包含了一段代码也包含了这段代码使用的数据,这里的数据也被称为context,即上下文,或者environment,其实就是函数运行依赖的数据;
这就是引入function 类型的原因,单纯的函数指针没有捕捉上下文的能力,这里的上下文就是指代码依赖的数据,我们不得不手动创建一个结构体来保存代码依赖的上下文,上面的定义的结构体本质上可以理解为一个类;
单纯的函数指针并没有捕捉上下文的能力,这里的上下文就是指代码依赖的数据,你不得不自己动手构造出一个结构体用来存储代码依赖的上下文,在C++中你没有办法单纯的利用函数指针指向对象的成员函数,就是因为函数指针没有办法捕捉this(指向对象的指针)这个上下文
function 类型就是为了解决这个问题引入的
利用std::function你不但可以保存一段代码,同时也可以保存必要的上下文,然后在合适的地方基于上下文调用这段代码;
同时std::function也更加通用,你可以用其存储任何可以被调用的对象(callable object),也就是可以存储仿函数对象

function 类型的定义

function 类型支持下面四种类型的赋值

  • 普通函数
  • 类成员函数
  • 仿函数
  • 匿名函数(lamda 表达式)
  1. 普通函数的 函数指针vs函数对象
uint32_t testNormalFunction(uint32_t a) {
    cout << "testNormalFunction a: " << a << endl;
    return a;
}

{
    //normal function pointer
    uint32_t(*funcptr)(uint32_t) = testNormalFunction;
    (*funcptr)(10); // testNormalFunction a:10
    funcptr(10); // testNormalFunction a:10 这种形式也是可以的
}
  1. 类成员函数
    注意类成员函数使用函数对象时,需要传入对象的实例,可以是指针或者引用
    注意函数的原型定义完全是在function 的模板参数中定义的
class demoFooClass{
public:
    demoFooClass() = default;
    ~demoFooClass() = default;
    uint32_t testPrintFunction(uint32_t a) {
        return a + magicNumber;
    }
    uint32_t magicNumber = 10;
};

//传入对象的指针
{
    demoFooClass *p = new demoFooClass();
    function<uint32_t(demoFooClass*, uint32_t)> mfptr = &demoFooClass::testPrintFunction;
    uint32_t val = mfptr(p, p->magicNumber);
    printf("mfptr using pointer value is val: %d\n", val);
}

//传入对象的引用
{
    demoFooClass p;
    function<uint32_t(demoFooClass&, uint32_t)> mfptr = &demoFooClass::testPrintFunction;
    uint32_t val = mfptr(p, p.magicNumber);
    printf("mfptr using reference value is val: %d\n", val);
}
  1. 仿函数
    仿函数作为函数类型时,实际上就是绑定重载()的函数,本质上和绑定类成员函数的原理是一样的
class demoMyTestClass {
public:
    demoMyTestClass() = default;
    ~demoMyTestClass() = default;
    uint32_t magicNumber = 1;
    uint32_t operator()(uint32_t a) {
        return a + magicNumber;
    }
};

{
    demoMyTestClass p;
    function<uint32_t(demoMyTestClass&, uint32_t)> mftestptr = &demoMyTestClass::operator();
    uint32_t val = mftestptr(p, p.magicNumber);
    printf("mftestptr val: %d\n", val);
}
  1. lamda 表达式
    lamda 表达式本质上是一种匿名函数,所属的类型为function 类型
{
    uint32_t temp = 100;
    function<uint32_t(uint32_t)> p = [=](uint32_t a)->uint32_t {
        return a + temp;
    };

    uint32_t val = p(10);
    printf("p val: %d\n", val); // 110
}

std::bind/std::placeholder

std::bind 的作用是绑定函数参数,它解决的问题是:我们有时候不一定能够一次性获取调用某个函数的全部参数,通过 std::bind,我们可以将部分调用参数体腔绑定到函数身上成为一个新的对象,此时的对象函数的参数个数发生了变化,等到最参数全部齐全后,再完成函数的调用
std::placeholder 作为占位符,表示的是还没有确定的参数,注意std::placeholder 顺序可以改变实际调用参数的顺序

  1. 通过 placeholder 改变实际参数的顺序
class demoFooClass{
public:
    demoFooClass() = default;
    ~demoFooClass() = default;
    uint32_t testPrintFunction(uint32_t a, uint32_t b, uint32_t c) {
        cout << " testPrintFunction a:" << a << " b:" << b << " c:" << c << endl;
        uint32_t sum = a + b + c;
        return sum;
    }
    uint32_t magicNumber = 10;
};

{
    demoFooClass p;
    function<uint32_t(demoFooClass&, uint32_t, uint32_t, uint32_t)> mftestptr = &demoFooClass::testPrintFunction;
    
    // normal use
    auto mBindptr = bind(mftestptr, p, placeholders::_1, placeholders::_2, placeholders::_3);
    mBindptr(1,2,3); // a:1 b:2 C:3

    // change order 
    auto nBindptr = bind(mftestptr, p, placeholders::_2, placeholders::_1, placeholders::_3);
    nBindptr(1, 2, 3);  // a:2 b:1 C:3
}
  1. 通过 placeholder 改变实际参数的个数
{
    demoFooClass p;
    function<uint32_t(demoFooClass&, uint32_t, uint32_t, uint32_t)> mftestptr = &demoFooClass::testPrintFunction;
    auto mBindptr = bind(mftestptr, p, placeholders::_1, placeholders::_2, 100);
    mBindptr(1, 2);

    auto nBindptr = bind(mftestptr, p, placeholders::_1, 200, 100);
    nBindptr(1);
}

std::function 是一种通用性的,多态性质的函数封装,它的实例可以对任何可以调用的目标进行存储、复制和调用操作,它也是C++中现有的可调用实体的一种类型安全的包裹(相对来说,函数指针的调用不是类型安全的),换句话说,就是函数的容器,我们有了函数对象之后能够更加方便的将函数指针,函数本体作为对象处理

lamda 表达式

lamda 表达式就是一个函数(匿名函数),也就是一个没有函数名的函数;因为我们使用它是一次性的,允许直接在原位构造的方式使用它,所以不需要名字
lamda 表达式也叫闭包,闭就是封闭的意思,表示其他地方不会调用它,包就是函数的意思
lamda 本质上是一个函数对象,其内部自动创建了一个重载()操作符的类

lamda 表达式基本形式如下:


lamda形式.jpg
变量捕获
  • [] 不捕获任何变量,此时lamda表达式不能访问任何外部变量
  • [&] 函数体内可以使用 lambda 所在范围内所有可见的局部变量(包括 lambda 所在类的 this),并且是引用传递方式
    (相当于是编译器自动为我们按引用传递了所有局部变量)
  • [=] 函数体内可以使用 lambda 所在范围内所有可见的局部变量(包括 lambda 所在类的 this),并且是值传递方式(相
    当于编译器自动为我们按值传递了所有局部变量)
  • [=, &a] 以引用的方式捕获a,其余变量按照值的方式进行传递
  • [&, a] 以值的方式捕获a,其余变量按照引用的方式进行传递
  • [a] 以值的方式捕获a,不捕获其他变量
  • [this] 捕获所在类的 this 指针(qt 中使用很多,如此lamda 表达式可以通过this 访问界面空间的数据)
函数参数定义

标识重载的 () 操作符的参数,没有参数时这部分可以省略,参数可以通过按值(如: (a, b))和按引用 (如: (&a, &b)) 两种方式进行传递

mutable 或 exception 声明

这部分可以省略,按值传递函数对象参数时,加上 mutable 修饰符后,可以修改传递进来的拷贝(注意是能修改拷贝,而不是值本身)exception 声明用于指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw(int)

返回值类型

标识函数返回值的类型,当返回值为 void或者函数体中只有一处 return 的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略

std::string mstr = "MagicStr";
uint32_t mVal = 100;

{
    uint32_t temp = 10;
    auto fptr = [=](uint32_t x, uint32_t y) {
        printf("magic str: %s temp value: %d \n", mstr.c_str(), temp);
        mstr = "hello";
        mVal = 0;
        //temp = 0; //compile error   [=](uint32_t x, uint32_t y) mutable {
        return x + y;
    };
    // get fptr value: 30 mstr:hello mVal:100 temp:10
    printf("get fptr value: %d mstr:%s mVal:%d temp:%d \n", fptr(10, 20), mstr.c_str(), mVal, temp);
}

{
    uint32_t temp = 100;
    auto fptr = [&](uint32_t x, uint32_t y) {
        printf("magic str: %s temp value: %d \n", mstr.c_str(), temp);
        mstr = "world";
        mVal = 0;
        temp = 0;
        return x + y;
    };
    // get fptr value: 30 mstr:world mVal:0 temp:100
    printf("get fptr value: %d mstr:%s mVal:%d temp:%d \n", fptr(10, 20), mstr.c_str(), mVal, temp);
}

  1. 如果是以值传递的方式,那么直接修改局部变量的值会编译出错,如果想修改局部变量的拷贝,需要加上mutable 关键字
  2. 使用引用传递是可以修改类成员变量的值的

在捕获参数的情况下,lamda 的使用方法如下:

[] (int x, int y) { return x + y; } // 隐式返回类型
[] (int& x) { ++x;  } // 没有 return 语句 -> Lambda 函数的返回类型是 'void'
[] () { ++global_x;  } // 没有参数,仅访问某个全局变量
[] { ++global_x; } // 与上一个相同,省略了 (操作符重载函数参数)

使用lamda 表达式的优势之一就是可以与容器和算法库相结合

  1. 使用lamda 表达式用一行代码对容器数据求和
//使用lamda 表达式求和
{
    uint32_t sum = 0;
    vector<uint32_t> somelist{10, 20 ,30 ,40 ,50};
    for_each(somelist.begin(), somelist.end(), [&sum](uint32_t x) {
        sum += x;
    });
    printf("Get vector somelist sum: %d \n", sum); // 150

}
  1. 循环打印容器内容
{
    vector<uint32_t> somelist{ 10, 20 ,30 ,40 ,50 };
    for_each(somelist.begin(), somelist.end(), [](uint32_t x) {
        printf(" %d", x);
    });
    printf("\n");
}

相关文章

  • java8 lambda-2-各类语言中的lambda

    C/C++:函数指针 C#:委托 java之前:接口参数,实际传入匿名对象(匿名内部类) lamda表达式是Jav...

  • C++ lambda表达式与函数对象

    C++ lambda表达式与函数对象 lambda表达式是C++11中引入的一项新技术,利用lambda表达式可以...

  • Java8教程

    本次只讲三个东西,Lamda表达式、函数引用、函数式接口。 一、Lamda表达式 也就是说以前需要用匿名实现类来做...

  • 2021-12-10

    拓展函数 高阶函数 内联函数 lamda表达式 函数式编程 jetpack kotlin 协程 flow bind...

  • C++ lambda 表达式及表达式捕获

    1. 概述 C++ 11 中的 Lambda 表达式用于定义并创建匿名的函数对象,以简化编程工作。Lambda 的...

  • bind函数模版

    一、std::bind包装器/适配器介绍 1、函数模版bind生成f的可调用函数对象包装器,调用此包装器等价于...

  • JDK1.8之Lamda表达式(匿名内部类优化)

    1.Lamda表达式的作用 例如一个匿名内部类的使用,使用Lamda表达式可以简化。 图一Lamda表达式和图二的...

  • Java8 One---- Lamda表达式

    1 Lamda表达式 lamda表达式构成, 参数,箭头,方法体.为什么使用lamda表达式, 最只直观的就是简洁...

  • c++11系列-lambda表达式的实现

    c++增加了function对象,对所有的可调用对象的抽象表达: 函数指针 lambda表达式 bind表达式 函...

  • Lamda 表达式作用域和内置函数式接口

    Lamda 表达式作用域 访问局部变量 可以直接在 lambda 表达式中直接访问外部的局部变量: 但是和匿名对象...

网友评论

      本文标题:C++ 11(3) ---- 函数对象包装器和lamda 表达式

      本文链接:https://www.haomeiwen.com/subject/wugirdtx.html