Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Bind temporary to non-const reference

Rationale

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:

Problem Description

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.

Ugly Workaround

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)?

like image 486
Konrad Rudolph Avatar asked Feb 02 '12 10:02

Konrad Rudolph


2 Answers

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.

like image 76
Matthieu M. Avatar answered Oct 21 '22 20:10

Matthieu M.


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.)

like image 44
James Kanze Avatar answered Oct 21 '22 21:10

James Kanze