How to translate C file pointers into generic C++ streams?

Viewed 143

Consider the following C code:

void openFile(const char *mode, char *filename, FILE **fileptr)
{
  ...
  *fileptr = fopen(filename, mode);
  ...
}

FILE *logstream;
if (LOG_FILE_ENABLED)
{
  openFile("w", "mylogfile.txt", logstream);
}
else
{
  logstream = stderr;
}

fprintf(logstream, "[DEBUG] Some debug message...\n");
fclose(logstream);

I am attempting to translate this to idiomatic C++. How can I overload openFile() such that it takes a std::ofstream, but keep logstream stream-agnostic? I was assuming it would be something like this:

void openFile(const char *mode, char *filename, std::ofstream &ofs)
{
  ...
  ofs.open(filename);
  ...
}

std::ostream logstream;
if (LOG_FILE_ENABLED)
{
  logstream = std::ofstream();
  openFile("w", "mylogfile.txt", logstream);
}
else
{
  logstream = std::cerr;
}

logstream << "[DEBUG] Some debug message..." << std::endl;
logstream.close();

However this is apparently wildly incorrect - you can't even initialize a plain std::ostream like that. How should I handle this - preferably while avoiding the use of raw pointers?

3 Answers

I would move the actual work to a separate function or lambda that takes a std::ostream as input. The caller can then decide which type of std::ostream to pass in, eg:

void doRealWork(std::ostream &log)
{
    ...
    log << "[DEBUG] Some debug message..." << std::endl;
    ...
}

if (LOG_FILE_ENABLED)
{
  std::ofstream log("mylogfile.txt");
  doRealWork(log);
}
else
{
  doRealWork(std::cerr);
}

Or:

auto theRealWork = [&](std::ostream &&log)
{
    ...
    log << "[DEBUG] Some debug message..." << std::endl;
    ...
}

if (LOG_FILE_ENABLED) {
  theRealWork(std::ofstream{"mylogfile.txt"});
} else {
  theRealWork(static_cast<std::ostream&&>(std::cerr));
}

UPDATE: Otherwise, you can do something more like this instead:

using unique_ostream_ptr = std::unique_ptr<std::ostream, void(*)(std::ostream*)>;

unique_ostream_ptr logstream;
if (LOG_FILE_ENABLED) {
  logstream = unique_ostream_ptr(new std::ofstream("mylogfile.txt"), [](std::ostream *strm){ delete strm; });
} else {
  logstream = unique_ostream_ptr(&std::cerr, [](std::ostream *){});
}

*logstream << "[DEBUG] Some debug message...\n";

Or:

using shared_ostream_ptr = std::shared_ptr<std::ostream>;

shared_ostream_ptr logstream;
if (LOG_FILE_ENABLED) {
  logstream = std::make_shared<std::ofstream>("mylogfile.txt");
} else {
  logstream = shared_ostream_ptr(&std::cerr, [](std::ostream*){});
}

*logstream << "[DEBUG] Some debug message...\n";

C++ stream library has pretty ancient design. Nevertheless - its basic idea is that ostream or istream are just wrapper objects over stream-buffers.

So you might try something like in this code:

std::ostream get_log(bool str) {
    if (str) return std::ostream(new std::stringbuf());
     // else
    std::filebuf* f = new std::filebuf();
    f->open("log", std::ios_base::out);
    return std::ostream(f);
}

But, as I mentioned, this is very ancient design - so no RAII - this buffer is not owned by stream - you would need to delete it by yourself:

int main() {
    std::ostream log = get_log(true);
    log << "aaa";
    std::cout << static_cast<std::stringbuf&>(*log.rdbuf()).str();
    
    delete log.rdbuf(); // (!)
}

So this is not very usable.

So my final advice - use smart pointer over ostream - like this:

std::unique_ptr<std::ostream> get_log(bool str) {
    if (str) return new std::ostringstream();
    std::ofstream* f = new std::ofstream();
    f->open("log", std::ios_base::out);
    return f;
}

int main() {
    auto log = get_log(true);
    *log << "aaa";
}

You were nearly there; you just need to be mindful of scoping rules, and of the fact that there is no such thing as an std::ostream other than as an abstract base class.

So:

std::ostream* logstreamPtr = nullptr;
std::ofstream ofs;
if (LOG_FILE_ENABLED)
{
  logstreamPtr = &ofs;
  openFile("w", "mylogfile.txt", ofs);
}
else
{
  logstreamPtr = &std::cerr;
}

std::ostream& logstream = *logstreamPtr;

logstream << "[DEBUG] Some debug message..." << std::endl;
logstream.close();

You don't need the reference logstream, but it saves you from having to repeatedly reference logstreamPtr later, which would be boring.

Don't be afraid of this raw pointer. This is the purest application of pointers there is. You can go down the smart pointer route if you like, but you gain nothing and lose readability (and, in some cases, performance).

By the way, if you're worried about performance, don't open and close the log file for every single message; that's extremely wasteful.

Related