Consider the following struct:
template<typename T>
struct stream
{
using type = decltype(
std::declval<std::ostream>() << std::declval<T>()
);
};
template<typename T>
using stream_t = typename stream<T>::type;
The "value" of stream_t<T>
when using certain built-in types (int
, float
, ...) for T
is std::ostream&
, as I expected.
But when using std::string
, char
, int*
, or some streamable dummy struct for T
, the type is an rvalue reference, std::ostream&&
.
Once std::declval<std::ostream>()
(returns an std::ostream&&
) is replaced withstd::declval<std::ostream&>
(returns an std::ostream&
, due to reference collapsing rule, right?) the returned type is the expected std::ostream&
. Is there some rvalue overload of operator<<
that I don't know about?
Why is this happening?
The results above are obtained with AppleClang 11.0.0.11000033. When using gcc-7.4 instead, the result is always std::ostream&
, as expected.
#include <iostream>
#include <type_traits>
/* ************************************
* Sans reference
* ************************************ */
template<typename T>
struct stream
{
using type = decltype(
std::declval<std::ostream>() << std::declval<T>()
);
};
template<typename T>
using stream_t = typename stream<T>::type;
/* ************************************
* With reference
* ************************************ */
template<typename T>
struct stream_ref
{
using type = decltype(
std::declval<std::ostream&>() << std::declval<T>()
);
};
template<typename T>
using stream_ref_t = typename stream_ref<T>::type;
/* ************************************
* Dummy struct
* ************************************ */
struct Dummy
{
friend std::ostream& operator<<(std::ostream&, const Dummy&);
};
/* ************************************
* Static asserts
* ************************************ */
static_assert( std::is_same_v<stream_t<int>, std::ostream&> );
static_assert( std::is_same_v<stream_t<float>, std::ostream&> );
static_assert( std::is_same_v<stream_t<std::string>, std::ostream&&> );
static_assert( std::is_same_v<stream_t<const char*>, std::ostream&&> );
static_assert( std::is_same_v<stream_t<int*>, std::ostream&&> );
static_assert( std::is_same_v<stream_t<Dummy>, std::ostream&&> );
static_assert( std::is_same_v<stream_ref_t<std::string>, std::ostream&> );
static_assert( std::is_same_v<stream_ref_t<const char*>, std::ostream&> );
static_assert( std::is_same_v<stream_ref_t<int*>, std::ostream&> );
static_assert( std::is_same_v<stream_ref_t<Dummy>, std::ostream&> );
int main(int argc, char** argv)
{
return 0;
}
Actually this behavior is not Apple Clang specific, but common for all modern C++ compilers including GCC, Clang, MSVC, which all accept your program. Demo: https://gcc.godbolt.org/z/8ex6Pc9nb
These checks
static_assert( std::is_same_v<stream_t<std::string>, std::ostream&&> );
static_assert( std::is_same_v<stream_t<const char*>, std::ostream&&> );
static_assert( std::is_same_v<stream_t<int*>, std::ostream&&> );
static_assert( std::is_same_v<stream_t<Dummy>, std::ostream&&> );
are valid because here the global function template returning rvalue-reference:
template< class Ostream, class T >
Ostream&& operator<<( Ostream&& os, const T& value );
is selected, see (3) in https://en.cppreference.com/w/cpp/io/basic_ostream/operator_ltlt2
And these checks
static_assert( std::is_same_v<stream_t<int>, std::ostream&> );
static_assert( std::is_same_v<stream_t<float>, std::ostream&> );
are satisfied because member functions basic_ostream<T>::operator<<
are preferred for int
and float
arguments, and these member functions return lvalue-reference:
https://en.cppreference.com/w/cpp/io/basic_ostream/operator_ltlt