Why does taking `istream&` to a temporary `stringstream` work, but not when taking `stringstream&`?

Consider the following code, it compiles and runs:

#include <iostream>
#include <sstream>
struct Foo {};
void operator>>(std::istream &, Foo) {
}
int main() {
    std::stringstream{} >> Foo{};
}

However, if I change std::istream to std::stringstream, I get an error:

c.cpp: In function 'int main()':
c.cpp:7:25: error: no match for 'operator>>' (operand types are 'std::stringstream' {aka 'std::__cxx11::basic_stringstream<char>'} and 'Foo')
    7 |     std::stringstream{} >> Foo{};
      |          ~~~~~~~~~~~~~~ ^~ ~~~~~
      |          |                 |
      |          |                 Foo
      |          std::stringstream {aka std::__cxx11::basic_stringstream<char>}
c.cpp:4:6: note: candidate: 'void operator>>(std::stringstream&, Foo)' (near match)
    4 | void operator>>(std::stringstream &, Foo) {
      |      ^~~~~~~~
c.cpp:4:6: note:   conversion of argument 1 would be ill-formed:
c.cpp:7:10: error: cannot bind non-const lvalue reference of type 'std::stringstream&' {aka 'std::__cxx11::basic_stringstream<char>&'} to an rvalue of type 'std::stringstream' {aka 'std::__cxx11::basic_stringstream<char>'}
    7 |     std::stringstream{} >> Foo{};
      |          ^~~~~~~~~~~~~~

which makes sense: I cannot bind a lvalue reference to a rvalue (temporary) object.

Why does the first code compile?

UPD: my compiler is

g++ (Rev2, Built by MSYS2 project) 10.3.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

and flags are -std=gnu++17 -Wall -Wextra -Wshadow -O2

UPD2: at the very end there is an error regarding the operator which Brian Bi is talking about:

In file included from C:/Software/msys64/mingw64/include/c++/10.3.0/iostream:40,
                 from c.cpp:1:
C:/Software/msys64/mingw64/include/c++/10.3.0/istream:980:5: note: candidate: 'template<class _Istream, class _Tp> typename std::enable_if<std::__and_<std::__not_<std::is_lvalue_reference<_Tp> >, std::__is_convertible_to_basic_istream<_Istream>, std::__is_extractable<typename std::__is_convertible_to_basic_istream<_Tp>::__istream_type, _Tp&&, void> >::value, typename std::__is_convertible_to_basic_istream<_Tp>::__istream_type>::type std::operator>>(_Istream&&, _Tp&&)'
  980 |     operator>>(_Istream&& __is, _Tp&& __x)
      |     ^~~~~~~~
C:/Software/msys64/mingw64/include/c++/10.3.0/istream:980:5: note:   template argument deduction/substitution failed:
C:/Software/msys64/mingw64/include/c++/10.3.0/istream: In substitution of 'template<class _Istream, class _Tp> typename std::enable_if<std::__and_<std::__not_<std::is_lvalue_reference<_Tp> >, std::__is_convertible_to_basic_istream<_Istream>, std::__is_extractable<typename std::__is_convertible_to_basic_istream<_Tp>::__istream_type, _Tp&&, void> >::value, typename std::__is_convertible_to_basic_istream<_Tp>::__istream_type>::type std::operator>>(_Istream&&, _Tp&&) [with _Istream = std::__cxx11::basic_stringstream<char>; _Tp = Foo]':
c.cpp:7:32:   required from here
C:/Software/msys64/mingw64/include/c++/10.3.0/istream:980:5: error: no type named 'type' in 'struct std::enable_if<false, std::basic_istream<char>&>'

Answer

As strange as it might seem, this code is well-formed. It should always compile. See Godbolt. It should also compile when the operator>> overload is changed to take std::stringstream&.

The reason is that there exists a special rvalue operator>> overload for classes derived from std::ios_base, marked (3) here:

template< class Istream, class T >
Istream&& operator>>( Istream&& st, T&& value );

The effect is equivalent to:

st >> std::forward<T>(value);
return std::move(st);

This is the operator>> overload that is called by your code. It delegates to the operator>> that you’ve written. (Because Foo is a member of the global namespace, unqualified name lookup will find your operator>> from any context thanks to ADL.)

If you have a toolchain that doesn’t have the rvalue stream extraction operator, then your code will not compile because, obviously, a non-const lvalue reference, Base&, will never bind to an rvalue of type Derived (where Derived is derived from Base).

I don’t know exactly why the rvalue stream extraction operator exists in the standard library. I think it’s because someone realized that there’s no good reason for is >> x not to work if is happens to be an rvalue expression of stream type. But many of the existing operator>>s were free functions taking an lvalue reference to basic_istream as their first argument. So adding this rvalue operator>> overload, which delegates to an lvalue one, was the solution to this problem. According to this point of view, the fact that your code compiles is not a bug: it’s intentional that you can write an operator>> that takes a non-const lvalue reference to stream type and have it work on rvalues too.