简体   繁体   中英

declval expression (for SFINAE) with std::ostream

I'm trying to create a type traits class to determine if a particular type T can be streamed via the << operator of an std::ostream . I'm using a straightforward SFINAE technique.

Ultimately, the expression I attempt to evaluate for substitution failure is:

decltype(std::declval<std::ostream>() << std::declval<T>()) ;

My expectation is that, given an instance t of type T and an std::ostream instance os , if the expression os << t is ill-formed, a substitution failure should occur.

But apparently substitution failure never occurs here regardless of the type T . And even if I just declare a typedef using the above decltype expression, outside the context of SFINAE, it happily compiles, even if T cannot be used with std::ostream .

For example:

struct Foo  { };

int main()
{
    // This compiles fine using GCC 4.9.2
    //
    typedef decltype(
        std::declval<std::ostream>() << std::declval<Foo>()
    ) foo_type;
}

The above will compile fine using GCC 4.9.2, which is not what I expected since the << operator is not overloaded to work with a type Foo . And of course, if I say:

std::cout << Foo();

... I get a compiler error. So why does the decltype expression above even compile at all?

C++11 added the following operator<< overload:

template< class CharT, class Traits, class T >
basic_ostream< CharT, Traits >& operator<<( basic_ostream<CharT,Traits>&& os, 
                                            const T& value );

This forwards on to the standard insertion operators, which can't bind rvalue references to std::ostream s because they take non-const references. Since std::declval<std::ostream> returns std::ostream&& , this overload is selected, then due to the very permissive interface (ie this isn't SFINAEd out if there is no valid underlying insertion operator), your decltype specifier works.

The simple fix is to use std::declval<std::ostream&>() . This will return a std::ostream& , so the template overload will not be selected by your decltype specifier and a normal insertion operator overload will be required for it to compile:

typedef decltype(
    std::declval<std::ostream&>() << std::declval<Foo>()
) foo_type;

Clang outputs this:

main.cpp:8:39: error: invalid operands to binary expression ('std::basic_ostream<char>' and 'Foo')
        std::declval<std::ostream&>() << std::declval<Foo>()
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^  ~~~~~~~~~~~~~~~~~~~

Live Demo


Here's a simpler example which exhibits the same problem:

#include <string>

void foo (int&,int){}
void foo (int&,float){}

template <typename T>
void foo (int&& a, T b) {
    foo(a, b);
}

int main()
{
    std::string s;
    typedef decltype(foo(1,s)) foo_type;
}

Live Demo


Here are the relevant standards quotes (N4140):

The declaration must be instantiated because overload resolution is involved:

[temp.inst]/10: If a function template or a member function template specialization is used in a way that involves overload resolution, a declaration of the specialization is implicitly instantiated (14.8.3).

Only the declaration needs to be instantiated:

[temp.over]/5: Only the signature of a function template specialization is needed to enter the specialization in a set of candidate functions. Therefore only the function template declaration is needed to resolve a call for which a template specialization is a candidate.

And the implementation is not allowed to instantiate the function body:

[temp.inst]/11: An implementation shall not implicitly instantiate a function template, a variable template, a member template, a non-virtual member function, a member class, or a static data member of a class template that does not require instantiation.

Doesn't really answer why this is happening but if you replace with std::stream& as below:

template<typename T, typename Enable = std::ostream&>
struct can_be_streamed : std::false_type {};
template<typename T>
struct can_be_streamed<T, 
         decltype(std::declval<std::ostream&>() << std::declval<T>())> : std::true_type {};

seems to work.

Live Demo

If you look at the header file ostream , you will find that because std::declval produces rvlaue references, there is actually a matching generic operator<< :

#if __cplusplus >= 201103L
  /** 
   *  @brief  Generic inserter for rvalue stream
   *  @param  __os  An input stream.
   *  @param  __x  A reference to the object being inserted.
   *  @return  os  
   *   
   *  This is just a forwarding function to allow insertion to
   *  rvalue streams since they won't bind to the inserter functions
   *  that take an lvalue reference.
  */  
  template<typename _CharT, typename _Traits, typename _Tp>
    inline basic_ostream<_CharT, _Traits>&
    operator<<(basic_ostream<_CharT, _Traits>&& __os, const _Tp& __x)
    {   
      __os << __x;
      return __os;
    }   
#endif // C++11

This explains why you get no substition failure. However, this can't actually be matched against the std::cout << Foo() call. Here is the relevant part of the compilation error:

/usr/local/bin/../lib/gcc/x86_64-pc-linux-gnu/6.1.0/../../../../include/c++/6.1.0/ostream:628:5: note: candidate function [with _CharT = char, _Traits = std::char_traits<char>, _Tp = Foo] not viable: no known conversion from 'ostream' (aka 'basic_ostream<char>') to 'basic_ostream<char, std::char_traits<char> > &&' for 1st argument
    operator<<(basic_ostream<_CharT, _Traits>&& __os, const _Tp& __x)
    ^

The problem here is that the lhs can only be an rvalue reference, but you're (obviously) using an lvalue (ie std::cout ) in the call.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM