Vincular referencia temporal a no constante

Razón fundamental

I try to avoid assignments in C++ code completamente. 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:

Descripción del problema

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

preguntado el 02 de febrero de 12 a las 10:02

Por qué static_cast se necesita aquí? -

@n.m.: The compiler can't see through the ?:. Both sides of the : debe ser del mismo tipo. -

"Aren’t streams in a way ill-designed?" Yes, very much so. -

@VJovic I’s not really relevant for the question but it’s just reading until it reaches the end of the stream, and storing the result in one contiguous string. -

I guess the main issue is that C++ was not made with this style in mind. In a Haskell tool, I replaced stdin with a file stream via a recursive function when a filename was passed, but I don't think this is appropriate here. -

4 Respuestas

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.

Respondido 02 Feb 12, 16:02

+1 An excellent solution. I'm using it a lot in Python at present, but curiously enough, it didn't occur to me to do the same in C++. - James Kanze

La solución con el 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 devolver un 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 trabajado.

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

Respondido 02 Feb 12, 15:02

+1 I like the MultiFileInputStream solution best. If streams API doesn't solve your problems, add a shim on top. :) - vhallac

The streams API does solve the problem. You just need to extend the implementation. (MultiFileInputStream hereda de std::istream. iostreams were designed with extension in mind, and I can't think of an application where we didn't have at least one custom streambuf and any number of custom manipulators.) - James Kanze

All SSA-style languages need to have phi nodes to be usable, realistically. You would run into the same problem in any case where you need to construct from two different types depending on the value of the condition. The ternary operator cannot handle such cases. Of course, in C++11 there are other tricks, like moving the stream or suchlike, or using a lambda, and the design of IOstreams is virtually the exact antithesis of what you're trying to do, so in my opinion, you would just have to make an exception.

Respondido 02 Feb 12, 16:02

Thanks, I didn’t know the name of this general problem (apparently I need to re-read the Dragon Book). A phi function for iostreams is indeed what I need, and moving might be an appropriate solution. Awesome, learned something interesting. - Konrad Rudolph

Another option might be an intermediate variable to hold the stream:

std::istream&& is = argc==1? std::move(cin) : std::ifstream(argv[1]);
std::string const input = slurp(is);

Taking advantage of the fact that named rvalue references are lvalues.

Respondido 02 Feb 12, 16:02

I can’t wrap my head around why this is legal. How does this make sure that the destructor of the ifstream is called at the end of the scope? - Konrad Rudolph

@Konrad: Are you familiar with the reference-to-const rules when binding a temporary? They apply here to. The lifetime of the temporary ifstream object is extended just the same as if it was bound to std::istream const&, and the destructor is called when the reference goes out of scope. The advantage of using an rvalue ref here is, that you can modify the object as you please. - Xeo

I am familiar with that. But if that applies here then won’t std::cin’s destructor be called twice if it’s bound to is in your code? I mean, normally it wouldn’t but what exactly is the type of the conditional expression, and how does it affect the compiler’s decision whether to bind the object’s lifetime to is’ scope? - Konrad Rudolph

The objects destroyed are not the same. One is the original X("a"), the other is the newly created object, which is the target of the move operation. See ideone.com/s9L62 for a more complete picture. - vhallac

@vhallac: Thanks, that makes sense. Too bad though, since this answer is now realmente useless... who wants a moved-from standard input? :( - Xeo

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.