6

As far as I understand C++11 references, I should not be able to bind an rvalue reference to a (non-const) lvalue reference as the former might be bound to a temporary and the latter must never be bound to a temporary.

However I found this odd behaviour in conjunction temporary stream objects (which I reduced as far as I could)

struct Dummy {};
template <typename Stream>
Stream& operator<<(Stream& s, Dummy) {
  return s << ".";          // <- (A)
}

template <typename Stream>
void pass(Stream&& s) {
  std::move(s) << Dummy();  // <- (X)   rvalue->lvalue conversion?
}

#include <fstream>
int main() {
  pass(std::fstream("test",std::ios::out));
}

If I write s << Dummy() in line (X), C++ complains in line (A), saying

error: invalid initialization of reference of type ‘std::basic_fstream<char>&’ from expression of type ‘std::basic_ostream<char>’

However, why does the code (as shown above) compiles and works as expected? The rvalue reference returned by std::move should just be as unable to being bound to an lvalue reference as the expression s is, but both gcc 4.6.1 and gcc 4.7.2 react identically.

And why does this phenomenon only appear to work with streams? When directly passing a Dummy&& to a function that expects a T& fails both with and without std::move.

bitmask
  • 32,434
  • 14
  • 99
  • 159

1 Answers1

11

basic_ostream has an overload of operator<< that looks like this:

template <typename Elem, typename Traits, typename T>
basic_ostream<Elem, Traits>&
    operator<<(basic_ostream<Elem, Traits>&& sink, const T& val)
{
    return sink << val;
}

This is called "Rvalue stream insertion" in the standard, at §27.7.3.9 [ostream.rvalue].

It allows implicit conversion (of sorts) from an rvalue basic_ostream to an lvalue. It was introduced specifically to allow temporary streams to be usable without resorting to tricks.


As for why the compile fails when you omit the move:

When Stream& operator<<(Stream& s, Dummy) is called without the move, Stream will be std::fstream which inherits from std::ostream (i.e. basic_ostream<char>).

It will use the basic_ostream<E, T>& operator<<(basic_ostream<E, T>&, const char*) overload to insert your string, then try to return the result of that expression which will be an ostream. You cannot implicitly downcast from std::ostream& to std::fstream&, so you get an error.

You can fix this by returning s on it's own line (where it won't have been implicitly upcasted.)

This isn't a problem with move because you go through that rvalue-to-lvalue insertion operator we just discovered first. Inside that function, the stream is a basic_ostream and so Stream is as well, and the return types will match.

Community
  • 1
  • 1
GManNickG
  • 494,350
  • 52
  • 494
  • 543
  • 1
    I see. That does make sense. But why is `std::move` needed at all, then? – bitmask Feb 14 '13 at 01:40
  • @bitmask: Because without it `s` cannot bind to an rvalue reference, given that it is an lvalue. – Kerrek SB Feb 14 '13 at 01:43
  • @KerrekSB: But it has a perfectly good overload for an rvalue referenced stream to bind to (as shown in this answer). Why doesn't that overload match without encouragement from `std::move`? – bitmask Feb 14 '13 at 01:47
  • @bitmask: That's not quite enough. Check out [this example](http://ideone.com/cDBJNy): If you remove `std::move`, it will no longer compile. (But I missed the important point about the downcast which you get because of your return type. You can try changing the operator to include a `static_cast`.) – Kerrek SB Feb 14 '13 at 09:35
  • @KerrekSB: I simply changed the implementation of `operator<<` to return its parameter in an extra statement. It strikes me as a lot better than unnecessary casts (I don't like casts). – bitmask Feb 14 '13 at 12:56
  • @GManNickG Does the conversion of the rvalue reference to an lvalue reference include any risks? I was told this "Rvalue stream insertion" pattern would cause unsolved problems and should not be used anymore, but it wasn't explained why. – Silicomancer Jul 22 '20 at 21:05
  • 1
    @Silicomancer I am unsure. I too have heard these claims, but have not seen a concrete reason why. I'm sure it's possible this could cause problems in some sense of the word "possible" (e.g., a contrived example), but I expect this to more or less just work with typical code. – GManNickG Jul 23 '20 at 01:58