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. 第7章 字符串, 流和正则表达

迭代器进行打印——std::ostream

使用输出流进行打印是一件很容易的事情,STL中的大多数基本类型都对operator<<操作符进行过重载。所以使用std::ostream_iterator类,就可以将数据类型中所具有的的元素进行打印,我们已经在之前的章节中这样做了。

本节中,我们将关注如何将自定义的类型进行打印,并且可以通过模板类进行控制。对于调用者来说,无需写太多的代码。

How to do it...

我们将对一个新的自定义的类使用std::ostream_iterator,并且看起来其具有隐式转换的能力,这就能帮助我们进行打印:

  1. 包含必要的头文件,并声明所使用的命名空间:

    #include <iostream>
    #include <vector>
    #include <iterator>
    #include <unordered_map>
    #include <algorithm>
    
    using namespace std;
    using namespace std::string_literals;
  2. 让我们实现一个转换函数,其会将数字和字符串相对应。比如输入1,就会返回“one”;输入2,就会返回“two”,以此类推:

    string word_num(int i) {
  3. 将会对哈希表进行填充,我们后续可以对它进行访问:

        unordered_map<int, string> m {
            {1, "one"}, {2, "two"}, {3, "three"},
            {4, "four"}, {5, "five"}, //...
        };
  4. 现在可以使用哈希表的find成员函数,通过传入相应的键值,返回对应的值。如果find函数找不到任何东西,我们就会得到一个"unknown"字符串:

        const auto match (m.find(i));
        if (match == end(m)) { return "unknown"; }
        return match->second;
    };
  5. 接下来我们就要定义一个结构体bork。其仅包含一个整型成员,其可以使用一个整型变量进行隐式构造。其具有print函数,其能接受一个输出流引用,通过borks结构体的整型成员变量,重复打印"bork"字符串:

    struct bork {
        int borks;
    
        bork(int i) : borks{i} {}
    
        void print(ostream& os) const {
            fill_n(ostream_iterator<string>{os, " "},
                   borks, "bork!"s);
        }
    };
  6. 为了能够更方便的对bork进行打印,对operator<<进行了重载,当通过输出流对bork进行输出时,其会自动的调用bork::print:

    ostream& operator<<(ostream &os, const bork &b) {
        b.print(os);
        return os;
    }
  7. 现在来实现主函数,先来初始化一个vector:

    int main()
    {
        const vector<int> v {1, 2, 3, 4, 5};
  8. ostream_iterator需要一个模板参数,其能够表述哪种类型的变量我们能够进行打印。当使用ostream_iterator<T>时,其会使用ostream& operator(ostream&, const T&)进行打印。这也就是之前在bork类型中重载的输出流操作符。我们这次只对整型数字进行打印,所以使用ostream_iterator<int>。使用cout进行打印,并可以将其作为构造参数。我们使用循环对vector进行访问,并且对每个输出迭代器i进行解引用。这也就是在STL算法中流迭代器的用法:

        ostream_iterator<int> oit {cout};
        for (int i : v) { *oit = i; }
        cout << '\n';
  9. 使用的输出迭代器还不错,不过其打印没有任何分隔符。当需要空格分隔符对所有打印的元素进行分隔时,我们可以将空格作为第二个参数传入输出流构造函数中。这样,其就能打印"1, 2, 3, 4, 5, ",而非"12345"。不过,不能在打印最后一个数字的时候将“逗号-空格”的字符串丢弃,因为迭代器并不知道哪个数字是最后一个:

        ostream_iterator<int> oit_comma {cout, ", "};
    
        for (int i : v) { *oit_comma = i; }
        cout << '\n';
  10. 为了将其进行打印,我们将值赋予一个输出流迭代器。这个方法可以和算法进行结合,其中最简单的方式就是std::copy。我们可以通过提供begin和end迭代器来代表输入的范围,在提供输出流迭代器作为输出迭代器。其将打印vector中的所有值。这里我们会将两个输出循环进行比较:

       copy(begin(v), end(v), oit);
       cout << '\n';
    
       copy(begin(v), end(v), oit_comma);
       cout << '\n';
  11. 还记得word_num函数吗?其会将数字和字符串进行对应。我们也可以使用进行打印。我们只需要使用一个输出流操作符,因为我们不需要对整型变量进行打印,所以这里使用的是string的特化版本。使用std::transfrom替代std::copy,因为需要使用转换函数将输入范围内的值转换成其他值,然后拷贝到输出中:

        transform(begin(v), end(v),
                 ostream_iterator<string>{cout, " "}, word_num);
        cout << '\n';
  12. 程序的最后一行会对bork结构体进行打印。可以直接使用,也并不需要为std::transform函数提供任何转换函数。另外,可以创建一个输出流迭代器,其会使用bork进行特化,然后再调用std::copy。bork实例可以通过输入范围内的整型数字进行隐式创建。然后,将会得到一些有趣的输出:

        copy(begin(v), end(v),
             ostream_iterator<bork>{cout, "\n"});
    }
  13. 编译并运行程序,就会得到以下输出。前两行和第三四行的结果非常类似。然后,会得到数字对应的字符串,然后就会得到一堆bork!字符串。其会打印很多行,因为我们使用换行符替换了空格:

    $ ./ostream_printing
    12345
    1, 2, 3, 4, 5,
    12345
    1, 2, 3, 4, 5,
    one two three four five
    bork!
    bork! bork!
    bork! bork! bork!
    bork! bork! bork! bork!
    bork! bork! bork! bork! bork!

How it works...

作为一个语法黑客,我们应知道std::ostream_iterator可以用来对数据进行打印,其在语法上为一个迭代器,对这个迭代器进行累加是无效的。对其进行解引用会返回一个代理对象,这些赋值操作符会将这些数字转发到输出流中。

输出流迭代器会对类型T进行特化(ostream_iterator<T>),对于所有类型的ostream& operator<<(ostream&, const T&)来说,都需要对其进行实现。

ostream_iterator总是会调用operator<<,通过模板参数,我们已经对相应类型进行了特化。如果类型允许,这其中会发生隐式转换。当A可以隐式转换为B时,我们可以对A类型的元素进行迭代,然后将这些元素拷贝到output_iterator<B>的实例中。我们会对bork结构体做同样的事情:bork实例也可以隐式转换为一个整数,这也就是我们能够很容易的在终端输出一堆bork!的原因。

如果不能进行隐式转换,可使用std::treansform和word_num函数相结合,对元素类型进行转换。

Note:

通常,对于自定义类型来说,隐式转换是一种不好的习惯,因为这是一个常见的Bug源,并且这种Bug非常难找。例子中,隐式构造函数有用程度要超过其危险程度,因为相应的类只是进行打印。

Previous迭代器填充容器——std::istreamNext使用特定代码段将输出重定向到文件

Last updated 6 years ago

Was this helpful?