Search code examples
c++c++11templatessfinaedeclval

Type trait to check if istream operator>> exists for given type


I found this type trait which can be used to check if a certain type T supports operator<<:

template<class Class>
struct has_ostream_operator_impl {
    template<class V>
    static auto test(V*) -> decltype(std::declval<std::ostream>() << std::declval<V>());
    template<typename>
    static auto test(...) -> std::false_type;

    using type = typename std::is_same<std::ostream&, decltype(test<Class>(0))>::type;
};

template<class Class>
struct has_ostream_operator : has_ostream_operator_impl<Class>::type {};

Source: https://gist.github.com/szymek156/9b1b90fe474277be4641e9ef4666f472

This works fine. Now I'm trying to do the same thing for operator>> using c++11, but I don't get it to work:

template<class Class>
struct has_istream_operator_impl {
  template<class V>
  static auto test(V*) -> decltype(std::declval<V>() >> std::declval<std::istream>());
  template<typename>
  static auto test(...) -> std::false_type;

  using type = typename std::is_same<std::istream&, decltype(test<Class>(0))>::type;
};

/**
 * @brief Type trait to check if operator>>(std::istream, Type) is defined for a given type.
 */
template<class Class>
struct has_istream_operator : has_istream_operator_impl<Class>::type {};

Here is a simplified test for my use case:

#include <sstream>
#include <type_traits>
#include <iostream>

// <include snippet 2>

template<typename T>
typename std::enable_if<has_istream_operator<T>::value, T>::type
fromString(const std::string& str) {
  T value;
  std::istringstream stream(str);
  stream >> value;
  return value;
}

int main() {
  std::cout << fromString<long>("123") + 1 << std::endl; // expecting 124
  return 0;
}

Error is:

has_istream_operator.cpp: In function ‘int main()’:
has_istream_operator.cpp:57:38: error: no matching function for call to ‘fromString<long int>(const char [4])’
   std::cout << fromString<long>("123") + 1 << std::endl; // expecting 124
                                      ^
has_istream_operator.cpp:49:1: note: candidate: ‘template<class T> typename std::enable_if<has_istream_operator<Class>::value, T>::type fromString(const string&)’
 fromString(const std::string& str) {
 ^~~~~~~~~~
has_istream_operator.cpp:49:1: note:   template argument deduction/substitution failed:
has_istream_operator.cpp: In substitution of ‘template<class T> typename std::enable_if<has_istream_operator<Class>::value, T>::type fromString(const string&) [with T = long int]’:
has_istream_operator.cpp:57:38:   required from here
has_istream_operator.cpp:49:1: error: no type named ‘type’ in ‘struct std::enable_if<false, long int>’

From which I understand that the SFINAE condition is false and therefore no definition of fromString exists.

What I have tried is to play around with the line

static auto test(V*) -> decltype(std::declval<V>() >> std::declval<std::istream>());

inside my definition of struct has_istream_operator_impl.

This is the variation which makes most sense to me, because when I use the operator>>, I usually do it this way: stream >> value for example and from my (limited) understanding, the test(V*) should test this generically for V:

static auto test(V*) -> decltype(std::declval<std::istream>() >> std::declval<V>());

But it does also not work (same error).

How do get I this to work?


Solution

  • Long story short, you should change

    static auto test(V*) -> decltype(std::declval<V>() >> std::declval<std::istream>());
    

    to

    static auto test(V*) -> decltype(std::declval<std::istream>() >> std::declval<V&>());
    

    There were two errors in the code, due to the following.

    • The >> operator takes the stream as first argument, not as second argument, whereas you are passing the two arguments the other way around.
    • declval<V>() is generating an rvalue (well, not really a value because we are in an unevaluated context), to which you can't assign a value via >> (just like you can't cin >> 123;), so you have to change it to declval<V&>().(¹)

    Update

    In reality, prompted by @brahmin's comment, I have to point out another bug in the original code. Follow this reasoning:

    • The first overload (including my correction) has_istream_operator_impl<T>::test is correctly chosen if >> makes sense between an std::istream (any value category) and a T& (lvalue);
    • however, has_istream_operator_impl<T>::type will incorrectly test if the return type of the call to test<T>(0) is std::istream&.
    • Doing so is a mistake, because there's many overloads of operator>> and they don't all return std::istream&:
      static_assert(std::is_same_v<
        decltype(std::declval<std::istream>() >> std::declval<std::string&>()),
        std::istream&&>);
      static_assert(std::is_same_v<
        decltype(std::declval<std::istream>() >> std::declval<int&>()),        
        std::istream&>);
      

    What you want to do, is to have has_istream_operator_impl<T>::type be std::true_type or std::false_type only depending on whether std::declval<std::istream>() >> std::declval<V&>() is valid , not on depending on its return type!

    One typical approach to do so is to use decltype with two arguments (or to static_cast<void> the result of one-argument decltype)

        static auto test(V*)
          -> decltype(std::declval<std::istream>() >> std::declval<V&>(), void());
    
        using type = typename std::is_same<void, decltype(test<Class>(0))>::type;
    

    So the correct code would be the following

    template<typename T>
    struct has_istream_operator_impl {
      template<typename V>
      static auto test(void*) -> decltype(std::declval<std::istream>() >> std::declval<V&>(), void());
      template<typename>
      static auto test(...) -> std::false_type;
    
      static constexpr bool value = std::is_same_v<decltype(test<T>(nullptr)), void>;
    };
    
    template<typename T>
    constexpr bool has_istream_operator = has_istream_operator_impl<T>::value;
    

    where I've made some cosmetic changes:

    • made has_istream_operator a value rather than a type, so the usage is less verbose:
      static_assert(has_istream_operator<int>);
      static_assert(!has_istream_operator<int*>);
      static_assert(has_istream_operator<std::string>);
      
    • used void* rather than V* for the "positve" test function parameter, as that has nothing to do with with V,
    • changed 0 to nullptr because we are passing it to a function taking a pointer,
    • changed class to typename as using both is confusing or at least distracting, imho.

    ¹ To understand more in depth why that's the case, look at the possible implementation of std::declval as shown at the documentation page on cppreference: as you can see, it returns the type typename std::add_rvalue_reference<T>::type (which, incidentally, can be written as std::add_rvalue_reference_t<T> since C++14), i.e. std::declval<T>() returns T&&, which is (by reference collapsing)

    • an lvalue reference if you provide an lvalue reference T to std::declval (e.g. std::declval<int&>()),
    • an rvalue reference otherwise (e.g. std::declval<int>()).

    In your usecase you are passing long as the T to std::declval, so we are in the second case, i.e. std::declval<long>() returns long&&. From the page on value categories you can see that an example of xvalue (which is, just like a prvalue, an rvalue) is the following (my emphasis):

    a function call or an overloaded operator expression, whose return type is rvalue reference to object, such as std::move(x);

    And that's exactly what std::declval<long>() is: it has a rvalue reference to object as its return type, hence it returns an rvalue.

    If you call, instead, std::declval<T&>() and pass long as T, the call will be std::declval<long&>(), in which case the return type will be long&, hence the call will return an lvalue. See the following quote, from the same page, of an example of lvalue

    a function call or an overloaded operator expression, whose return type is lvalue reference;

    To give an example of what std::declval is doing, these both pass

    static_assert(std::is_same_v<decltype(std::declval<int>()), int&&>);
    static_assert(std::is_same_v<decltype(std::declval<int&>()), int&>);
    

    and this fails to compile

    int x;
    std::cin >> static_cast<int&&>(x); // XXX
    

    where the // XXX line is what std::declval<std::istream>() >> std::declval<V>() is "emulating".