Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can I std::move a stream rvalue ref into an lvalue ref?

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.

like image 450
bitmask Avatar asked Feb 14 '13 01:02

bitmask


People also ask

Can rvalue bind to lvalue?

An lvalue reference can bind to an lvalue, but not to an rvalue.

Is an rvalue reference an lvalue?

An lvalue is an expression that yields an object reference, such as a variable name, an array subscript reference, a dereferenced pointer, or a function call that returns a reference. An lvalue always has a defined region of storage, so you can take its address. An rvalue is an expression that is not an lvalue.

Can you pass an lvalue to an rvalue reference?

In the example, the main function passes an rvalue to f . The body of f treats its named parameter as an lvalue. The call from f to g binds the parameter to an lvalue reference (the first overloaded version of g ). You can cast an lvalue to an rvalue reference.

Can you move a reference C++?

In C++11, the answer is--you can! That's what rvalue references and move semantics are for! Move semantics allows you to avoid unnecessary copies when working with temporary objects that are about to evaporate, and whose resources can safely be taken from that temporary object and used by another.


1 Answers

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.

like image 154
GManNickG Avatar answered Sep 19 '22 10:09

GManNickG