美文网首页
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");
    }
    

    相关文章

      网友评论

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

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