如何实现boost::iostreams中仅关闭时写入磁盘的Sink Device
我想要基于Boost.IOStreams实现一个延迟写入的Sink Device,核心需求是仅当流退出作用域时才将内容写入磁盘——因为直接用写入模式打开fstream会立刻清空文件,但实际场景中写入操作可能远晚于流打开,期间需要查看原文件内容。
按照官方教程编写的代码如下:
#include <exception> #include <fstream> #include <iosfwd> // streamsize #include <sstream> #include <string> #include <boost/iostreams/categories.hpp> // sink_tag #include <boost/iostreams/stream.hpp> class delayed_file_sink { std::string m_filename; std::ostringstream m_buf; public: typedef char char_type; typedef boost::iostreams::sink_tag category; std::streamsize write(const char* s, std::streamsize n) { auto cur = m_buf.str().size(); m_buf.write(s, n); auto now = m_buf.str().size(); return (now - cur); } delayed_file_sink(const std::string& filename) : m_filename{filename} { std::cout << "Constructor is called" << std::endl; } // does not compile without copy constructor delayed_file_sink(const delayed_file_sink& other) : m_filename{other.m_filename}, m_buf{other.m_buf.str()} { std::cout << "Copy Constructor is called" << std::endl; } ~delayed_file_sink() noexcept(false) { try { std::cout << "Destructor is called" << std::endl; std::fstream file(m_filename, std::ios_base::out); if (!file.good()) throw(std::runtime_error(std::string("Error opening file ") + m_filename)); file << m_buf.str(); } catch (const std::exception& e) { // Silent catch exception if nothing has been written and stack is unwinding if ((std::uncaught_exceptions() == 0) || (m_buf.str().size() > 0)) throw (e); } } }; using delayed_ofstream = boost::iostreams::stream<delayed_file_sink>; int main() { std::cout << "about to create object delayed_ofstream" << std::endl; delayed_ofstream info{"info.dat"}; std::cout << "about to exit" << std::endl; return 0; }
但创建delayed_ofstream对象后,文件会立即被清空。程序输出显示Boost库内部会3次复制该Sink对象,部分副本销毁时触发析构函数,导致文件被提前清空。
理想状态下这个设备应该不可复制,但如果不提供复制构造函数,代码无法编译。请问如何让Boost.IOStreams知道该设备不可复制,或者通过其他方式修复这个问题?
解决方案
Boost.IOStreams的stream模板默认要求设备具备可复制性,但可以通过以下几种方式解决复制导致的提前写入问题:
方法1:用device_wrapper包装不可复制设备
首先修改delayed_file_sink,删除复制构造和赋值运算符,标记为不可复制:
#include <boost/noncopyable.hpp> class delayed_file_sink : private boost::noncopyable { std::string m_filename; std::ostringstream m_buf; public: typedef char char_type; typedef boost::iostreams::sink_tag category; std::streamsize write(const char* s, std::streamsize n) { auto cur = m_buf.tellp(); m_buf.write(s, n); return m_buf.tellp() - cur; } delayed_file_sink(const std::string& filename) : m_filename{filename} { std::cout << "Constructor is called" << std::endl; } // 删除复制操作 delayed_file_sink(const delayed_file_sink&) = delete; delayed_file_sink& operator=(const delayed_file_sink&) = delete; ~delayed_file_sink() noexcept(false) { try { if (m_buf.str().empty()) return; // 空缓冲区不操作文件 std::cout << "Destructor is called" << std::endl; std::fstream file(m_filename, std::ios_base::out); if (!file.good()) throw std::runtime_error(std::string("Error opening file ") + m_filename); file << m_buf.str(); } catch (const std::exception& e) { if ((std::uncaught_exceptions() == 0) || !m_buf.str().empty()) throw e; } } };
然后定义delayed_ofstream时使用device_wrapper包装设备,让Boost持有设备引用而非复制:
using delayed_ofstream = boost::iostreams::stream<boost::iostreams::device_wrapper<delayed_file_sink>>;
创建对象时直接传递device_wrapper实例:
int main() { std::cout << "about to create object delayed_ofstream" << std::endl; delayed_ofstream info{boost::iostreams::device_wrapper<delayed_file_sink>("info.dat")}; std::cout << "about to exit" << std::endl; return 0; }
device_wrapper会避免Boost内部复制设备,只有原始设备销毁时才会触发写入逻辑。
方法2:让设备支持移动语义(不可复制但可移动)
修改delayed_file_sink,删除复制操作并添加移动构造/赋值,让Boost使用移动而非复制:
class delayed_file_sink { std::string m_filename; std::ostringstream m_buf; public: typedef char char_type; typedef boost::iostreams::sink_tag category; std::streamsize write(const char* s, std::streamsize n) { auto cur = m_buf.tellp(); m_buf.write(s, n); return m_buf.tellp() - cur; } delayed_file_sink(const std::string& filename) : m_filename{filename} { std::cout << "Constructor is called" << std::endl; } // 移动构造,转移缓冲区所有权后清空原对象缓冲区 delayed_file_sink(delayed_file_sink&& other) noexcept : m_filename{std::move(other.m_filename)}, m_buf{std::move(other.m_buf)} { std::cout << "Move Constructor is called" << std::endl; other.m_buf.str(""); } delayed_file_sink& operator=(delayed_file_sink&& other) noexcept { if (this != &other) { m_filename = std::move(other.m_filename); m_buf = std::move(other.m_buf); other.m_buf.str(""); std::cout << "Move Assignment is called" << std::endl; } return *this; } // 删除复制操作 delayed_file_sink(const delayed_file_sink&) = delete; delayed_file_sink& operator=(const delayed_file_sink&) = delete; ~delayed_file_sink() noexcept(false) { try { if (m_buf.str().empty()) return; std::cout << "Destructor is called" << std::endl; std::fstream file(m_filename, std::ios_base::out); if (!file.good()) throw std::runtime_error(std::string("Error opening file ") + m_filename); file << m_buf.str(); } catch (const std::exception& e) { if ((std::uncaught_exceptions() == 0) || !m_buf.str().empty()) throw e; } } };
此时boost::iostreams::stream会自动使用移动语义处理设备,不会创建多余副本,只有最终的设备对象析构时才会写入文件。
方法3:标记副本为非所有者,避免重复写入
如果需要保留可复制性,可以添加一个m_is_owner标记,只有原始对象析构时才执行写入:
class delayed_file_sink { std::string m_filename; std::ostringstream m_buf; bool m_is_owner = true; // 仅原始对象为所有者 public: typedef char char_type; typedef boost::iostreams::sink_tag category; std::streamsize write(const char* s, std::streamsize n) { auto cur = m_buf.tellp(); m_buf.write(s, n); return m_buf.tellp() - cur; } delayed_file_sink(const std::string& filename) : m_filename{filename} { std::cout << "Constructor is called" << std::endl; } // 复制构造时标记为非所有者 delayed_file_sink(const delayed_file_sink& other) : m_filename{other.m_filename}, m_buf{other.m_buf.str()}, m_is_owner{false} { std::cout << "Copy Constructor is called" << std::endl; } ~delayed_file_sink() noexcept(false) { if (!m_is_owner || m_buf.str().empty()) return; // 非所有者或空缓冲区跳过写入 try { std::cout << "Destructor is called" << std::endl; std::fstream file(m_filename, std::ios_base::out); if (!file.good()) throw std::runtime_error(std::string("Error opening file ") + m_filename); file << m_buf.str(); } catch (const std::exception& e) { if ((std::uncaught_exceptions() == 0) || !m_buf.str().empty()) throw e; } } };
这种方法保留了复制构造,但只有原始对象(m_is_owner为true)的析构会执行写入逻辑,副本销毁时不会触碰文件。
内容的提问来源于stack exchange,提问作者francesco

