Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

declval expression (for SFINAE) with std::ostream

Tags:

c++

sfinae

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?

like image 435
Siler Avatar asked May 27 '16 10:05

Siler


2 Answers

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::ostreams 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 (i.e. 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.

like image 193
TartanLlama Avatar answered Sep 28 '22 11:09

TartanLlama


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

like image 30
101010 Avatar answered Sep 28 '22 11:09

101010