Search code examples
c++boost-iostreams

sink device in boost::iostreams that writes to disk only when closed


I am attempting to write a "Sink Device" to be used with the boost::iostreams library. I want to implement a stream that writes on a file on the disk, but such a write is done only when the stream exits the scope. The idea is that the file is not touched until the actual writing is done. (The reason is that, in an actual code, the writing could happen much later in time than the opening of the stream, and in the time inbetween I would like to be able to inspect the file; if I just open an fstream in writing mode, it would be wiped immediately).

Following the tutorial I wrote a "Sink device" that waits to write on the disk until the destructor is called.

Here is a working code:

#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;
}

Contrary to my expectations, the file is wiped immediately after creating the delayed_ofstream object in main(). The reason can be understood by the output of the above program (where I have inserted some "prints" to monitor what is happening):

about to create object delayed_ofstream
Constructor is called
Copy Constructor is called
Copy Constructor is called
Copy Constructor is called
Destructor is called
Destructor is called
Destructor is called
about to exit
Destructor is called

This output indicates that, internally, the library is copying 3 times the object. Then some of the copies go out of the scope, and the destructor is called, which has the consequence of wiping the file.

Ideally, it would make sense to have such a device "uncopyable", but without the copy constructor the code does not compile.

Is there a way to tell the boost::iostreams library that the device is uncopyable? Or how can I otherwise fix the code?


Solution

  • Boost iostream devices need to be copyable (as you've discovered), the library predates movable types so copies devices a fair amount.

    To compensate for this devices are usually implemented with a shared_ptr holding their state. e.g. see basic_file which has a shared_ptr<impl> pimpl_ member.

    You should do something similar and have a shared impl class and write your file on destruction of that impl class rather than on destruction of each device.