1

I would like to write a function that returns an ostream that either writes to a file or stderr.

My first attempt:

#include <fstream>
#include <iostream>

std::ostream get_stream() {
    bool flag = (time(nullptr) % 2); // stand-in
    if (flag)
        return std::cerr;
    else
        return std::ofstream{"somefile.txt"};
}

int main() {
    auto logger {get_stream()};
    logger << "Just testing, everything is fine."
           << std::endl;
}

This fails with a (long) compiler error - I suspect because std::cerr does not have a copy constructor. Another variant (returning a reference) does not work because the ofstream is a local variable. I could allocate the ofstream on the heap, but then the caller doesn't know if the pointer needs to be freed (it doesn't when we return a reference to std:cerr).

So, I wrote version 2:

#include <fstream>
#include <iostream>

struct Logger {
    std::ofstream ostream;
    bool to_stderr {true};

    template<typename A>
    Logger& operator<<(A rhs) {
        if (to_stderr)
            std::cerr << rhs;
        else
            ostream << rhs;
        return this;
    }
};

int main() {
    Logger logger;
    logger << "Just testing, everything is fine."
           << std::endl;
}

This also fails with a long compile error that starts with:

$ g++ -Wall -o v2 v2.cpp
v2.cpp: In function ‘int main()’:
v2.cpp:30:12: error: no match for ‘operator<<’ (operand types are ‘Logger’ and ‘<unresolved overloaded function type>’)
     logger << "Just testing, everything is fine."
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            << std::endl;
            ^~~~~~
v2.cpp:9:13: note: candidate: template<class A> Logger& Logger::operator<<(A)
     Logger& operator<<(A rhs) {
             ^~~~~~~~
v2.cpp:9:13: note:   template argument deduction/substitution failed:
v2.cpp:30:20: note:   couldn't deduce template parameter ‘A’
            << std::endl;
                    ^~~~
[...]

Why isn't the second version working, and what is the right way to implement something like this?

(Using << operator to write to both a file and cout is a related question, but as far as I can tell the only difference in the solution given there is that operator<< is defined outside the class, and if I make the same change to my code, I'm still getting the same error)

Nikratio
  • 2,338
  • 2
  • 29
  • 43
  • 1
    You can't pass or return streams by value. –  Aug 24 '19 at 18:37
  • Also related: https://stackoverflow.com/q/56521318/5754656 – Artyer Aug 24 '19 at 18:38
  • The first one will work if `get_stream()` returns `std::ostream&`. – Pete Becker Aug 24 '19 at 19:10
  • @PeteBecker Returning a reference fixes the case where we want to return `cerr`, but it breaks returning the file stream because that is a local variable. – Nikratio Aug 24 '19 at 19:14
  • @Nikratio — so don’t make it a local variable. – Pete Becker Aug 24 '19 at 20:33
  • @PeteBecker If I allocate it on the heap, the caller doesn't know if the pointer needs to be freed or not. If I make it global, I can only have one active stream at any time (which works in the example, but not in general) – Nikratio Aug 27 '19 at 08:26
  • @Nikratio -- I'm not sure what you're trying to accomplish here. You've accepted an answer that explains how to do what you're objecting to here. – Pete Becker Aug 27 '19 at 12:16

2 Answers2

2

You just need to elevate the file stream to global status. This could be a global variable, a static member, or (my favorite) a block-local static variable. This approach ensures the file stream won't be opened until its first invocation, and will stay open until program termination.

From there you just return the stream by reference.

std::ostream &get_stream() {
    static std::ofstream file("somefile.txt");
    return flag ? std::cerr : file;
}
Cruz Jean
  • 2,761
  • 12
  • 16
  • Also note that in `main()`, the logger would need to be defined as a reference: `auto& logger` or `decltype(auto) logger`. – Justin Time - Reinstate Monica Aug 24 '19 at 22:07
  • Elevating this to a global variable is only possible because the example is deliberately simple. In the actual code, the file name varies and multiple streams may be in use at the same time. – Nikratio Aug 27 '19 at 08:25
2

You will have to dynamically-allocate your streams:

std::unique_ptr<std::ostream> get_stream() {
  bool flag = time(0) % 2;
  return ( flag
    ? std::make_unique<std::ostream>(std::cerr.rdbuf())
    : std::make_unique<std::ofstream>("somefile.txt")
  );
}

int main() {
  auto logger {get_stream()};
  *logger << "Just testing, everything is fine."
          << std::endl;
}
David G
  • 94,763
  • 41
  • 167
  • 253
  • This looks very promising, thanks! I think the real trick here is not to dynamically allocate, but to construct a new stream for `std:cerr` using `cerr.rdbuf`. Could you say a few more works about this? Is this always safe? Will writes to `std:cerr` and the "other" `cerr` be synchronized correctly? – Nikratio Aug 27 '19 at 08:27
  • @Nikratio No, they will not be synchronized (if I understand your meaning of the word synchronized). `std::cerr` is by default tied to `std::cout` so any output operation on `std::cerr` will flush `std::cout`, keeping them synchronized. This is not the case with the returned stream in this example. If you want a complete copy of `std::cerr` you would use `copyfmt` after calling the constructor. `? []{auto o=std::make_unique(std::cerr.rdbuf());o->copyfmt(std::cerr);return o;}()` – David G Aug 29 '19 at 01:43