I try to avoid assignments in C++ code completely. That is, I use only initialisations and declare local variables as const
whenever possible (i.e. always except for loop variables or accumulators).
Now, I’ve found a case where this doesn’t work. I believe this is a general pattern but in particular it arises in the following situation:
Let’s say I have a program that loads the contents of an input file into a string. You can either call the tool by providing a filename (tool filename
) or by using the standard input stream (cat filename | tool
). Now, how do I initialise the string?
The following doesn’t work:
bool const use_stdin = argc == 1;
std::string const input = slurp(use_stdin ? static_cast<std::istream&>(std::cin)
: std::ifstream(argv[1]));
Why doesn’t this work? Because the prototype of slurp
needs to look as follows:
std::string slurp(std::istream&);
That is, the argument i non-const
and as a consequence I cannot bind it to a temporary. There doesn’t seem to be a way around this using a separate variable either.
At the moment, I use the following solution:
std::string input;
if (use_stdin)
input = slurp(std::cin);
else {
std::ifstream in(argv[1]);
input = slurp(in);
}
But this is rubbing me the wrong way. First of all it’s more code (in SLOCs) but it’s also using an if
instead of the (here) more logical conditional expression, and it’s using assignment after declaration which I want to avoid.
Is there a good way to avoid this indirect style of initialisation? The problem can likely be generalised to all cases where you need to mutate a temporary object. Aren’t streams in a way ill-designed to cope with such cases (a const
stream makes no sense, and yet working on a temporary stream does make sense)?
Why not simply overload slurp
?
std::string slurp(char const* filename) {
std::ifstream in(filename);
return slurp(in);
}
int main(int argc, char* argv[]) {
bool const use_stdin = argc == 1;
std::string const input = use_stdin ? slurp(std::cin) : slurp(argv[1]);
}
It is a general solution with the conditional operator.
The solution with the if
is more or less the standard solution when
dealing with argv
:
if ( argc == 1 ) {
process( std::cin );
} else {
for ( int i = 1; i != argc; ++ i ) {
std::ifstream in( argv[i] );
if ( in.is_open() ) {
process( in );
} else {
std::cerr << "cannot open " << argv[i] << std::endl;
}
}
This doesn't handle your case, however, since your primary concern is to obtain a string, not to "process" the filename args.
In my own code, I use a MultiFileInputStream
that I've written, which
takes a list of filenames in the constructor, and only returns EOF when
the last has been read: if the list is empty, it reads std::cin
. This
provides an elegant and simple solution to your problem:
MultiFileInputStream in(
std::vector<std::string>( argv + 1, argv + argc ) );
std::string const input = slurp( in );
This class is worth writing, as it is generally useful if you often write Unix-like utility programs. It is definitly not trivial, however, and may be a lot of work if this is a one-time need.
A more general solution is based on the fact that you can call a
non-const member function on a temporary, and the fact that most of the
member functions of std::istream
return a std::istream&
—a
non const-reference which will then bind to a non const reference. So
you can always write something like:
std::string const input = slurp(
use_stdin
? std::cin.ignore( 0 )
: std::ifstream( argv[1] ).ignore( 0 ) );
I'd consider this a bit of a hack, however, and it has the more general
problem that you can't check whether the open (called by the constructor
of std::ifstream
worked.
More generally, although I understand what you're trying to achieve, I
think you'll find that IO will almost always represent an exception.
You can't read an int
without having defined it first, and you can't
read a line without having defined the std::string
first. I agree
that it's not as elegant as it could be, but then, code which correctly
handles errors is rarely as elegant as one might like. (One solution
here would be to derive from std::ifstream
to throw an exception if
the open didn't work; all you'd need is a constructor which checked for
is_open()
in the constructor body.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With