现代C++实战30讲
吴咏炜
前 Intel 资深软件架构师
立即订阅
3774 人已学习
课程目录
已更新 20 讲 / 共 30 讲
0/4登录后,你可以任选4讲全文学习。
课前必读 (2讲)
开篇词 | C++这么难,为什么我们还要用C++?
免费
课前必读 | 有关术语发音及环境要求
基础篇 (9讲)
01 | 堆、栈、RAII:C++里该如何管理资源?
02 | 自己动手,实现C++的智能指针
03 | 右值和移动究竟解决了什么问题?
04 | 容器汇编 I:比较简单的若干容器
05 | 容器汇编 II:需要函数对象的容器
06 | 异常:用还是不用,这是个问题
07 | 迭代器和好用的新for循环
08 | 易用性改进 I:自动类型推断和初始化
09 | 易用性改进 II:字面量、静态断言和成员函数说明符
提高篇 (9讲)
10 | 到底应不应该返回对象?
11 | Unicode:进入多文字支持的世界
12 | 编译期多态:泛型编程和模板入门
13 | 编译期能做些什么?一个完整的计算世界
14 | SFINAE:不是错误的替换失败是怎么回事?
15 | constexpr:一个常态的世界
16 | 函数对象和lambda:进入函数式编程
17 | 函数式编程:一种越来越流行的编程范式
18 | 应用可变模板和tuple的编译期技巧
现代C++实战30讲
登录|注册

16 | 函数对象和lambda:进入函数式编程

吴咏炜 2020-01-01
你好,我是吴咏炜。
本讲我们将介绍函数对象,尤其是匿名函数对象——lambda 表达式。今天的内容说难不难,但可能跟你的日常思维方式有较大的区别,建议你一定要试验一下文中的代码(使用 xeus-cling 的同学要注意:xeus-cling 似乎不太喜欢有 lambda 的代码😓;遇到有问题时,还是只能回到普通的编译执行方式了)。

C++98 的函数对象

函数对象(function object)[1] 自 C++98 开始就已经被标准化了。从概念上来说,函数对象是一个可以被当作函数来用的对象。它有时也会被叫做 functor,但这个术语在范畴论里有着完全不同的含义,还是不用为妙——否则玩函数式编程的人可能会朝着你大皱眉头的。
下面的代码定义了一个简单的加 n 的函数对象类(根据一般的惯例,我们使用了 struct 关键字而不是 class 关键字):
struct adder {
adder(int n) : n_(n) {}
int operator()(int x) const
{
return x + n_;
}
private:
int n_;
};
它看起来相当普通,唯一有点特别的地方就是定义了一个 operator(),这个运算符允许我们像调用函数一样使用小括号的语法。随后,我们可以定义一个实际的函数对象,如 C++11 形式的:
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《现代C++实战30讲》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(10)

  • 总统老唐
    2020第一课,吴老师新年好

    作者回复: 谢谢🙏。在这儿也顺祝所有的同学们新年好!😇🎈🎊

    2020-01-01
    3
  • 空气
    吴老师,我在工作中很经常用到function。文中讲到function对象的创建比较耗资源,能否介绍一下原因,或者可以参考哪些资料?确实要使用的话,是否有必要使用共享指针管理来减轻复制和转移消耗?
    如果lambda的推导类型不是function,那是什么类型呢?和function有什么区别?

    作者回复: 你如果不是频繁创建 function 对象的话,关系也不大吧。我觉得多考虑移动就行了。除非性能测试工具报告瓶颈就在这儿了,用智能指针去优化不太值(毕竟需要修改使用的代码)。

    每个 lambda 都有自己的独特类型,每次定义相当于编译器帮你产生了一个函数对象(就像这一讲里定义的那些函数对象一样)。

    具体如何实现,我倒没读到过相关的文章。你可以网上搜搜看,或者阅读标准库里的源码。

    2020-01-04
  • 橙子888
    最近项目里使用到了libgo这个C++写的协程库,示例代码中用到了好多老师今天讲的知识点:
    void foo()
    {
        printf("function pointer\n");
    }

    struct A {
        void fA() { printf("std::bind\n"); }
        void fB() { printf("std::function\n"); }
    };
    int main()
    {
        go foo;

        go []{
            printf("lambda\n");
        };

        go std::bind(&A::fA, A());

        std::function<void()> fn(std::bind(&A::fB, A()));
        go fn;
    }
    其中跟在"go"后面的内容总算能理解了,但是"go"的实现原理还是没搞懂,不知道后面协程这块的内容会不会有讲到。
    另外对老师今天讲的 ”一般而言,按值捕获是比较安全的做法。按引用捕获时则需要更小心些,必须能够确保被捕获的变量和 lambda 表达式的生命期至少一样长“ 这句话深有体会,我在项目里按值捕获指针给协程用,结果调试的时候就是各种随机的崩溃。。。

    作者回复: libgo 我没有任何使用经验,不过,看起来它和大部分库实现的协程一样,都是 stackful coroutine。我第 30 讲会讲的是会进入 C++20 的 stackless coroutine。

    每个 libgo 的协程都有自己的独立栈空间,因此,协程唤起和休眠时都需要进行栈切换。无栈协程则跟唤起者使用同一个栈。有栈的协程实现另外还有 libco、Boost.Coroutine2 等。

    2020-01-04
  • 李亮亮
    Microsoft Visual Studio Community 2019 版本 16.4.2,语言标准:C++17 例子编译不过,水平又菜,不会改。

    作者回复: 是说有线程的那个例子吗?我刚又试了,没问题的。

    你是不是没有设定语言标准为 c++17?但你评论里又说设了……不设是确实不行的。

    我编译的命令行是:

    cl /EHsc /std:c++17 test16.cpp

    如果你遇到错误了,又不贴出错误信息,别人也没法帮你啊……

    2020-01-02
    5
  • 廖熊猫
    老师新年快乐。
    lambda表达式大概是生成了一个匿名的struct吧,实现了operator(), 捕获的话对应struct上的字段。

    作者回复: 新年快乐。

    对,概念上就是这样。

    2020-01-02
  • tt
    1、感觉lambda表达式就是C++中的闭包。

    2、lambda表达式可以立即进行求职,这一点和JavaScript里的立即执行函数(Imdiately Invoked Function Expression,IIFE)一样。在JavaScript里,它是用来解决作用域缺陷的。

    感觉在动态语言里被用到极致的闭包等特性,因为C++的强大、完备,在C++里很普通。

    lambda的定义对应一个匿名函数对象,捕获就是构造这个对象时某种方式的初始化过程,用lambda表达式隐藏了这个过程,只保留了这个意思,更直观和写意。

    老师,我对协程很感兴趣,C++会有协程么?隐约感觉捕获变量这个东西是不是可以用在实现协程上?

    最后,祝老师新年快乐!

    作者回复: 对,就是闭包。

    Stackful 协程见 Boost.Coroutine2。Stackless 协程已经进入 C++20,第 30 讲讨论。🤓

    新年快乐!

    2020-01-02
  • 禾桃
    "请自行运行一下代码,并把 *this 改成 this,看看输出会有什么不同。"

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

    class task {
    public:
        task(int data) : data_(data) { cout << __func__ << "|" << this << endl; }
        auto lazy_launch()
        {
            return
                // *this 标明按值捕获外围对象
                // 变量名 = 表达式 标明按值捕获表达式的结果
                    [this, count = get_count()]()
                            mutable { // mutable 标记使捕获的内容可更改
                        cout << __func__ << "|" << this << endl;
                        ostringstream oss;
                        oss << "Done work " << data_
                            << " (No. " << count
                            << ") in thread "
                            << this_thread::get_id()
                            << '\n';
                        msg_ = oss.str();
                        calculate();
                    };
        }
        void calculate()
        {
            this_thread::sleep_for(100ms);
            cout << msg_;
        }

    private:
        int data_;
        string msg_;
    };

    int main()
    {
        auto t = task{37};
        thread t1{t.lazy_launch()};
        thread t2{t.lazy_launch()};
        t1.join();
        t2.join();
    }

     打印输出
    task|0x7ffe6f0e7120
    operator()|0x7ffe6f0e7120
    operator()|0x7ffe6f0e7120
    Done work 37 (No. 2) in thread 140331800897280
    Done work 37 (No. 2) in thread 140331800897280

    不太明白为什么,
    #1 t1, t2这两个thread有同样的thread id(140331800897280)?
    #2 为什么 count在, t1, t2运行时,打印出的都是2(No. 2)?

    多谢!

    作者回复: 就是让你想一想的呀。提示:按引用捕获的后果。

    2020-01-01
    5
  • 罗乾林
    编译器遇到lambda 表达式时,产生一个匿名的函数对象,各种捕获相当于按值或者按引用设置给匿名对象的成员字段。
    不对的地方,望老师指正。
    对function<int(int, int)>这货怎么实现的比较好奇,大多数模板参数都是类型,做的都是是类型推导,这货居然是int(int, int)

    作者回复: lambda表达式的理解没啥问题。

    int(int, int) 也是一个类型:一个接受两个整数参数、返回一个整数的函数。function 的主要复杂性,应该是需要处理函数、函数指针、函数对象等各种情况。函数对象的大小不确定,因而 function 需要在堆上分配内存。operator() 我记得相当于一个虚函数调用的复杂度。

    2020-01-01
  • hello world
    请问老师后续会讲关于类对象及虚函数表相关知识吗,这块比较薄弱

    作者回复: 不会。谈这个的书和文章够多了。

    2020-01-01
  • viper
    老师,为什么上面会说用add_2去调用那三模版函数返回值都是2,不该是4吗?

    作者回复: 谢谢反馈。已更正。

    2020-01-01
    1
收起评论
10
返回
顶部