I'm trying to make a simple logger class, and I want the ability to either log to either a generic ostream
(cout
/cerr
) or a file. The design I have in mind is to allow the constructor to either take an ostream&
or a filename, and in the latter case create an ofstream&
and assign that to the class' private ostream&
like so:
class Log {
private:
std::ostream& os;
public:
Log(std::ostream& os = std::cout): os(os) { }
Log(std::string filename) {
std::ofstream ofs(filename);
if (!ofs.is_open())
// do errorry things
os = ofs;
}
};
Doing such gives me an error that ofstream
's assignment operator is private. Looking over that again, it occurred to me that making a reference to a local object probably wouldn't work, and making os
a pointer to an ostream
and declaring and deleting it on the heap worked with the ofstream
case, though not with the ostream
case, where the ostream
already exists and is just being referenced by os
(because the only place to delete os
would be in the constructor, and I don't know of a way to determine whether or not os
is pointing to an ofstream
created on the heap or not).
So how can I make this work, i.e. make os
reference an ofstream
initialized with a filename in the constructor?
For one thing, you can't rebind references once they're created, you can only initialise them. You might think you could do this:
Log(std::string filename) : os(std::ofstream(filename)) {
if (!os.is_open())
// do errorry things
}
But that's no good because you are making os
refer to a temporary variable.
When you need a reference that has to be optional, that is, it needs to refer to something sometimes and not other times, what you really need is a pointer:
class Log {
private:
std::ostream* os;
bool dynamic;
public:
Log(std::ostream& os = std::cout): os(&os), dynamic(false) { }
Log(std::string filename) : dynamic(true) {
std::ofstream* ofs = new std::ofstream(filename);
if (!ofs->is_open())
// do errorry things and deallocate ofs if necessary
os = ofs;
}
~Log() { if (dynamic) delete os; }
};
The above example is just to show you what is going on, but you probably will want to manage it with a smart pointer. As Ben Voigt points out, there are a lot of gotchas that will cause unforeseen and undesired behaviour in your program; for example, when you try to make a copy of the above class, it will hit the fan. Here is an example of the above using smart pointers:
class Log {
private:
std::unique_ptr<std::ostream, std::function<void(std::ostream*)>> os;
public:
Log(std::ostream& os = std::cout): os(&os, [](ostream*){}) { }
Log(std::string filename) : os(new std::ofstream(filename), std::default_delete<std::ostream>()) {
if (!dynamic_cast<std::ofstream&>(*os).is_open())
// do errorry things and don't have to deallocate os
}
};
The unusual os(&os, [](ostream*){})
makes the pointer point to the given ostream&
but do nothing when it goes out of scope; it gives it a deleter function that does nothing. You can do this without lambdas too, it's just easier for this example.