You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现boost::iostreams中仅关闭时写入磁盘的Sink Device

问题:Boost.IOStreams延迟写入Sink设备的复制问题

我想要基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 09:09:51