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 (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.
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 love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With