Search code examples
c++c++17ostreamdecltype

Curious results from decltype( std::devlcal<std::ostream>() << std::declval<T>() ) when using AppleClang


Question

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?

Compiler specs

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.

Complete source

#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;
}

Solution

  • 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