7

I am trying to create a templated can_stream struct that inherits from std::false_type or std::true_type depending on whether operator<< is defined for type T.

#include <iostream>

struct S {
    int i;
};

std::ostream& operator<< (std::ostream& os, S const& s) {
    os << s.i << std::endl;
    return os;
};

struct R {
    int i;
};

template<
    typename T,
    typename Enable = void
> struct can_stream : std::false_type {
};

template<
    typename T
> struct can_stream< T, decltype( operator<<( std::declval< std::ostream& >(), std::declval< T const& >())) > : std::true_type {
};

int main() {
    std::cout << can_stream< int >::value << std::endl;
    std::cout << can_stream<  S  >::value << std::endl;
    std::cout << can_stream<  R  >::value << std::endl;
}

I thought the above program would produce

  1
  1
  0

because:

  • operator << exists for both int and S (so decltype(...) is well formed).
  • partially specialised template is a better match than the unspecialised template.

However, it produces:

  0
  0
  0

Why?

Boann
  • 48,794
  • 16
  • 117
  • 146
az5112
  • 590
  • 2
  • 11
  • 1
    Prefer the expression form. `std::declval< std::ostream& >() << std::declval< T const& >()` - Some built-in types are implemented by `std` as members of `ostream`. If you want to detect those too, you can't assume the operator is not a member. The expression takes care of all that for you. – StoryTeller - Unslander Monica Feb 16 '21 at 20:40

1 Answers1

8

operator << exists for both int and S (so decltype(...) is well formed).

But decltype( operator<<( std::declval< std::ostream& >(), std::declval< T const& >())) is std::ostream&, where the default value for Enable is void.

There isn't match.

You can try with

template<
    typename T // ........................................................................................VVVVVVVVV
> struct can_stream< T, decltype( operator<<( std::declval< std::ostream& >(), std::declval< T const& >()), void() ) > : std::true_type {
};

or, if you can use C++17, so std::void_t,

template<
    typename T // ......VVVVVVVVVVVV
> struct can_stream< T, std::void_t<decltype( operator<<( std::declval< std::ostream& >(), std::declval< T const& >()))> > : std::true_type {
};

This solve the problem with S, because for S there is an operator<<() function. But doesn't works for int because, for int, the operator isn't defined as function. So, for int, you have to simulate the use

template<
    typename T // ...........................................................VVVV
> struct can_stream< T, std::void_t<decltype( std::declval< std::ostream& >() << std::declval< T const& >() )> > : std::true_type {
};

See you if you prefer check the existence of a function operator<<() (but this doesn't works with int and other types with implicit operator <<) or if the operator << is concretely usable.

max66
  • 65,235
  • 10
  • 71
  • 111
  • 1
    this makes `can_stream::value == false` (https://godbolt.org/z/98e3hq) , but perhaps thats a different question – 463035818_is_not_an_ai Feb 16 '21 at 20:41
  • 3
    `decltype(std::declval() << std::declval(), void())` works for `int` too though – Ted Lyngmo Feb 16 '21 at 20:43
  • 2
    @largest_prime_is_463035818 I think that's because the form of `operator <<`. `std::declval< std::ostream& >() << std::declval< T const& >()` should makes it work – NathanOliver Feb 16 '21 at 20:43
  • "_But_ `decltype( operator<<(...)` _is_ `std::ostream&`, _where the default value for Enable is void_." I was assuming `= void` in `typename Enable = void` meant '_use type void if no type is specified explicitly_'. I did not realise the partial specialisation needed to have `Enable = void` in order to match. – az5112 Feb 17 '21 at 04:11