C++17 STL Cook Book
  • Introduction
  • 前言
  • 关于本书
  • 各章梗概
  • 第1章 C++17的新特性
    • 使用结构化绑定来解包绑定的返回值
    • 将变量作用域限制在if和switch区域内
    • 新的括号初始化规则
    • 构造函数自动推导模板的类型
    • 使用constexpr-if简化编译
    • 只有头文件的库中启用内联变量
    • 使用折叠表达式实现辅助函数
  • 第2章 STL容器
    • 擦除/移除std::vector元素
    • 以O(1)的时间复杂度删除未排序std::vector中的元素
    • 快速或安全的访问std::vector实例的方法
    • 保持对std::vector实例的排序
    • 向std::map实例中高效并有条件的插入元素
    • 了解std::map::insert新的插入提示语义
    • 高效的修改std::map元素的键值
    • std::unordered_map中使用自定义类型
    • 过滤用户的重复输入,并以字母序将重复信息打印出——std::set
    • 实现简单的逆波兰表示法计算器——std::stack
    • 实现词频计数器——std::map
    • 实现写作风格助手用来查找文本中很长的句子——std::multimap
    • 实现个人待办事项列表——std::priority_queue
  • 第3章 迭代器
    • 建立可迭代区域
    • 让自己的迭代器与STL的迭代器兼容
    • 使用迭代适配器填充通用数据结构
    • 使用迭代器实现算法
    • 使用反向迭代适配器进行迭代
    • 使用哨兵终止迭代
    • 使用检查过的迭代器自动化检查迭代器代码
    • 构建zip迭代适配器
  • 第4章 Lambda表达式
    • 使用Lambda表达式定义函数
    • 使用Lambda为std::function添加多态性
    • 并置函数
    • 通过逻辑连接创建复杂谓词
    • 使用同一输入调用多个函数
    • 使用std::accumulate和Lambda函数实现transform_if
    • 编译时生成笛卡尔乘积
  • 第5章 STL基础算法
    • 容器间相互复制元素
    • 容器元素排序
    • 从容器中删除指定元素
    • 改变容器内容
    • 在有序和无序的vector中查找元素
    • 将vector中的值控制在特定数值范围内——std::clamp
    • 在字符串中定位模式并选择最佳实现——std::search
    • 对大vector进行采样
    • 生成输入序列的序列
    • 实现字典合并工具
  • 第6章 STL算法的高级使用方式
    • 使用STL算法实现单词查找树类
    • 使用树实现搜索输入建议生成器
    • 使用STL数值算法实现傅里叶变换
    • 计算两个vector的误差和
    • 使用ASCII字符曼德尔布罗特集合
    • 实现分割算法
    • 将标准算法进行组合
    • 删除词组间连续的空格
    • 压缩和解压缩字符串
  • 第7章 字符串, 流和正则表达
    • 创建、连接和转换字符串
    • 消除字符串开始和结束处的空格
    • 无需构造获取std::string
    • 从用户的输入读取数值
    • 计算文件中的单词数量
    • 格式化输出
    • 使用输入文件初始化复杂对象
    • 迭代器填充容器——std::istream
    • 迭代器进行打印——std::ostream
    • 使用特定代码段将输出重定向到文件
    • 通过集成std::char_traits创建自定义字符串类
    • 使用正则表达式库标记输入
    • 简单打印不同格式的数字
    • 从std::iostream错误中获取可读异常
  • 第8章 工具类
    • 转换不同的时间单位——std::ratio
    • 转换绝对时间和相对时间——std::chrono
    • 安全的标识失败——std::optional
    • 对元组使用函数
    • 使用元组快速构成数据结构
    • 将void*替换为更为安全的std::any
    • 存储不同的类型——std::variant
    • 自动化管理资源——std::unique_ptr
    • 处理共享堆内存——std::shared_ptr
    • 对共享对象使用弱指针
    • 使用智能指针简化处理遗留API
    • 共享同一对象的不同成员
    • 选择合适的引擎生成随机数
    • 让STL以指定分布方式产生随机数
  • 第9章 并行和并发
    • 标准算法的自动并行
    • 让程序在特定时间休眠
    • 启动和停止线程
    • 打造异常安全的共享锁——std::unique_lock和std::shared_lock
    • 避免死锁——std::scoped_lock
    • 同步并行中使用std::cout
    • 进行延迟初始化——std::call_once
    • 将执行的程序推到后台——std::async
    • 实现生产者/消费者模型——std::condition_variable
    • 实现多生产者/多消费者模型——std::condition_variable
    • 并行ASCII曼德尔布罗特渲染器——std::async
    • 实现一个小型自动化并行库——std::future
  • 第10章 文件系统
    • 实现标准化路径
    • 使用相对路径获取规范的文件路径
    • 列出目录下的所有文件
    • 实现一个类似grep的文本搜索工具
    • 实现一个自动文件重命名器
    • 实现一个磁盘使用统计器
    • 计算文件类型的统计信息
    • 实现一个工具:通过符号链接减少重复文件,从而控制文件夹大小
Powered by GitBook
On this page
  • How to do it...
  • How it works...

Was this helpful?

  1. 第4章 Lambda表达式

使用Lambda为std::function添加多态性

我们现在想编写一些观察函数,用来观察一些变量的变化,当相应变量的数值发生改变时会进行提示,比如气压仪或是股票软件这类的东西。当有些值发生变化时,对应的观察对象就会被调用,之后以对应的方式进行反应。

为了实现这个观察器,我们存储了一些相关的函数对象在一个vector中,这些函数都接受以int变量作为参数,这个参数就是观察到的值。 我们不清楚这些函数对于传入值会做什么特殊的处理,不过我们也没有必要知道。

那么vector中的函数对象类型是什么呢?std::vector<void (*)(int)>,只要函数声明成void f(int)就符合这个这个函数指针类型的定义。这对于Lambda表达式同样有效,不过Lambda表达就是不能捕获任何值了——[](int x) {...}。对于捕获列表来说,Lambda表达式确实和普通的函数指针不同,因为其就不是一个函数指针,是一个函数对象,也就是将很多数据耦合到一个函数当中!想想在C++11时代之前,C++中没有Lambda表达式,类和结构体通常会将数据和函数耦合在一起,并且当你修改一个类中的数据成员时,你得到的是一个完全不同类型的数据。

这样vector中就无法将使用同样类型名字的不同类别的对象存储在一起。不能捕获已存在的变量,这个限制对于用户来说非常的不友好,也限制了代码的使用范围。用户该如何保存不同类型的函数对象呢?对接口进行约束,采用特定的传参方式传入已经观察到的值?

本节中,我们将展示使用std::function来解决这个问题,其将扮演一个“Lambda表达式多态包装器”的角色,捕获列表是不是空的都没有关系。

How to do it...

本节我们将创建很多Lambda表达式,其捕获类型是完全不同的,但是其函数签名的类型是相同的。然后,使用std::function将这些函数对象存入一个vector:

  1. 包含必要的头文件:

     #include <iostream>
     #include <deque>
     #include <list>
     #include <vector>
     #include <functional>
  2. 我们先实现一个简单的函数,其返回值是一个Lambda表达式。其需要传入一个容器,并且返回一个函数对象,这个函数对象会以引用的方式捕获容器。且函数对象本身接受传入一个整型参数。当向函数对象传入一个整型时,表达式将会把传入的整型,添加到捕获的容器尾部:

    template <typename C>
    static auto consumer (C &container)
        return [&] (auto value) {
            container.push_back(value);
        };
    }
  3. 另一个辅助函数将会打印传入的容器中所有的内容:

    template <typename C>
    static void print (const C &c)
    {
        for (auto i : c) {
            std::cout << i << ", ";
        }
        std::cout << '\n';
    }
  4. 主函数中,我们先实例化一个deque和一个list,还有一个vector,这些容器存放的元素都是int类型。

    int main()
    {
        std::deque<int> d;
        std::list<int> l;
        std::vector<int> v;
  5. 现在使用consumer函数对象与刚刚实例化的容器进行配合:将在vector中存储生成自定义的函数对象。然后,用一个vector存放着三个函数对象。每个函数对象都会捕获对应的容器对象。这些容器对象都是不同的类型,不过都是函数对象。所以,vector中的实例类型为std::function<void(int)>。所有函数对象都将隐式转换成一个std::function对象,这样就可以存储在vector中了。

        const std::vector<std::function<void(int)>> consumers
            {consumer(d), consumer(l), consumer(v)};
  6. 现在我们将10个整型值传入自定义函数对象:

        for (size_t i {0}; i < 10; ++i) {
            for (auto &&consume : consumers) {
                consume(i);
            }
        }
  7. 三个容器都包含了同样的10个整数。让我们来打印它们:

        print(d);
        print(l);
        print(v);
    }
  8. 编译运行程序,就会看到如下输出,和我们的期望是一样的。

    $ ./std_function
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
    0, 1, 2, 3, 4, 5, 6, 7, 8, 9,

How it works...

本节中比较复杂的地方就是这一行:

const std::vector<std::function<void(int)>> consumers
    {consumer(d), consumer(l), consumer(v)};

d,l和v对象都包装进一个consumer(...)调用中。这个调用会返回多个函数对象,这样每个函数对象都能捕获这三个容器实例。虽然函数对象只能接受int型变量为参数,但是其捕获到的是完全不同的类型。这就将不同类型的A、B和C变量存入到一个vector中一样。

为了这个功能,需要找到一个共同的类型,也就是能保存不同类型的函数对象,这个类型就是std::function。一个std::function<void(int)>对象可以存储我们的函数对象或传统函数,其接受只有一个整型参数和返回为空的函数类型。这里使用了多态性,为函数类型进行解耦。思考如下的写法:

std::function<void(int)> f (
    [&vector](int x) { vector.push_back(x); });

这里有个函数对象,将Lambda表达式包装入std::function对象当中,当我们调用f(123)时,会产生一个虚函数调用,其会重定向到对象内部的实际执行函数。

当存储函数对象时,std::function就显得非常智能。当我们使用Lambda表达式捕获越来越多的变量时,std::function实例的体积也会越来越大。如果对象体积特别特别巨大,那么其将会在堆上分配出对应内存空间来存放这个函数对象。这些对于我们代码的功能性并没有什么影响,这里需要让你了解一下是因为这样的存储方式会对性能有一定的影响。

Note:

很多初学者都认为或希望std::function<...>的实际表达类型是一个Lambda表达式。不过这是错误的理解!因为有多态库的帮助,其才能将Lambda表达式进行包装,从而抹去类型的差异。

Previous使用Lambda表达式定义函数Next并置函数

Last updated 6 years ago

Was this helpful?