C++ / a working model

83 / 163   ·   C++11   ·   约 14 分钟

streambuf 分层:给格式化输出换一个目的地

先记住这句话

ostream 负责把数值变成字符,streambuf 负责传递字符。用一个固定容量的自定义缓冲区接住同一套格式化操作,再制造确定的容量不足,观察设备层失败如何传成流的 badbit。

本篇内容
  1. 格式化与设备不该绑在一起
  2. put area 满了以后发生什么
  3. 检查流状态,也检查已经发生的写入
  4. 运行示例
  5. 动手练习
READING EVIDENCE / 已读部分正文

Standard C++ IOStreams and Locales: Advanced Programmer's Guide and Reference

完整阅读作者公开的 streambuf: The Stream Buffer Classes 授权书摘,标注摘自印刷页84—109,含层次结构、get/put area、string/file buffer、putback。此为选摘文章,不声称逐字等同书中26页。其余第1—3章、locale部分和参考手册未取得、未读。

查看版本、实际阅读范围与原文入口 →

格式化与设备不该绑在一起

如果输出目标从文件变成内存,整数转文本的规则不应该重写。书摘展示的关键边界是 stream 解释格式和状态,streambuf 提供字符序列的传输接口。自定义设备接入字符层,就能复用 ostream 的格式化能力。

示例的设备是一段固定大小数组。它没有文件、网络和动态分配,不支持定位,只保证从头顺序写入。把承诺限制清楚,比表面上实现完整设备却漏掉边界更可靠;固定容量也让失败路径可以确定地复现。

put area 满了以后发生什么

构造时用 setp 设置起点和尾后指针,正常字符可以直接进入 put area;空间耗尽时,后续写入无法继续。我们不把满缓冲区偷偷扩容,而是返回 traits_type::eof() 表示无法接收字符。

overflow 接收的是 int_type,不应先转为 char 再判断 EOF,否则会混淆字符和结束标记。示例对于 EOF 请求返回 not_eof,表示无需新增字符的处理成功;对于真的新字符返回 EOF。这个派生类没有待刷入另一设备的数据,字符数组本身就是最终目的地。

检查流状态,也检查已经发生的写入

一次输出失败不意味着之前的字符自动撤销。示例先成功写入三字符,然后追加第四个,断言 badbit 被设置且原有三个字符仍在;这同时检验上层状态和下层内容。需要整条消息原子提交的协议,应另加事务缓冲,不能把 ostream 误认为数据库。

缓冲对象必须比引用它的 ostream 活得久。本例按声明逆序销毁,先销毁流,再销毁缓冲。书中关于文件缓冲指针排列的图是解释模型,不是强制 ABI;C++20 中分层接口仍适用,但不要依赖某个标准库内部采用单缓冲还是双缓冲。

常见误区

  • badbit 不会撤销已输出字符;恢复流状态也不等于扩大设备容量。
  • ostream 不拥有传入的自定义 streambuf;先销毁缓冲再使用流会悬空。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <array>
#include <cassert>
#include <cstddef>
#include <locale>
#include <ostream>
#include <streambuf>
#include <string>
class FixedBuffer : public std::streambuf {
    std::array<char, 3> data_{};
protected:
    int_type overflow(int_type c = traits_type::eof()) override {
        if (traits_type::eq_int_type(c, traits_type::eof()))
            return traits_type::not_eof(c);
        return traits_type::eof();
    }
public:
    FixedBuffer() { setp(data_.data(), data_.data() + data_.size()); }
    FixedBuffer(const FixedBuffer&) = delete;
    FixedBuffer& operator=(const FixedBuffer&) = delete;
    std::string text() const { return std::string(pbase(), pptr()); }
};
int main() {
    FixedBuffer buffer;
    std::ostream out(&buffer);
    out.imbue(std::locale::classic());
    out << 42 << '!';
    assert(out.good());
    assert(buffer.text() == "42!");
    out.put('?');
    assert(out.bad());
    assert(buffer.text() == "42!");
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-iostreams-locales.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

把容量从3改成4,保留前两次写入,再追加一个字符会怎样?

查看参考答案

数组改为 std::array<char, 4> 后,输出 42! 再 put(?) 都成功,内容为 42!?;再调用 put(#) 才设置 badbit,内容保持 42!?。相应断言应在第四字符后检查 good(),第五字符后检查 bad(),这检验的是容量边界而不是调用次数。

继续查证

标准草案与官方章节会更新;版本标记只说明示例最低要求。

回到目录