Declval (для SFINAE) с std:: ostream

Я пытаюсь создать класс признаков типов, чтобы определить, может ли определенный тип T быть потоковым с помощью << оператора std::ostream. Я использую простой метод SFINAE.

В конечном итоге выражение, которое я пытаюсь оценить для отказа замены, следующее:

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

Мое ожидание состоит в том, что, учитывая экземпляр T типа T и std::ostream instance os, если выражение os << t плохо сформировано, должен произойти сбой замены.

Но, очевидно, сбой замены не возникает здесь, независимо от типа T. И даже если я просто объявляю typedef с использованием вышеприведенного выражения decltype, вне контекста SFINAE, он с радостью компилируется, даже если T не может использоваться с std::ostream.

Например:

struct Foo  { };

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

Выше будет компилироваться с использованием GCC 4.9.2, чего я не ожидал, так как оператор << не перегружен для работы с типом Foo. И, конечно, если я скажу:

std::cout << Foo();

... Я получаю ошибку компилятора. Итак, почему выражение decltype выше даже вообще компилируется?

Ответы

Ответ 1

С++ 11 добавил следующую перегрузку operator<<:

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

Это относится к стандартным операторам вставки, которые не могут связывать ссылки rvalue с std::ostream, потому что они принимают неконстантные ссылки. Поскольку std::declval<std::ostream> возвращает std::ostream&&, эта перегрузка выбрана, то из-за очень разрешительного интерфейса (т.е. Это не SFINAEd, если нет действительного основного оператора вставки), ваш спецификатор decltype работает.

Простым решением является использование std::declval<std::ostream&>(). Это вернет a std::ostream&, поэтому перегрузка шаблона не будет выбрана вашим спецификатором decltype, и для его компиляции потребуется перегрузка оператора с нормальной загрузкой:

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

Clang выводит это:

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


Здесь приведен более простой пример, который показывает ту же проблему:

#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


Вот соответствующие котировки стандартов (N4140):

Объявление должно быть инстанцировано, поскольку задействовано разрешение перегрузки:

[temp.inst]/10: Если шаблон функции или спецификация шаблона функции-члена используются таким образом, что это связано с перегрузкой разрешение, декларация специализации неявно создается (14.8.3).

Необходимо создать только декларацию:

[temp.over]/5: Для ввода специализации в набор требуется только подпись специализации шаблона функции кандидатские функции. Поэтому для разрешения вызова требуется только объявление шаблона функции. специалистом по шаблону является кандидат.

И реализации не разрешается создавать экземпляр тела функции:

[temp.inst]/11: Реализация не должна имплицитно создавать шаблон функции, шаблон переменной, шаблон-член, не виртуальную функцию-член, класс-член или статический элемент данных шаблона класса, который не требует создания экземпляра.

Ответ 2

На самом деле не отвечает, почему это происходит, но если вы замените на std::stream&, как показано ниже:

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 {};

похоже, работает.

Live Demo

Ответ 3

Если вы посмотрите на заголовочный файл ostream, вы обнаружите, что, поскольку std::declval создает ссылки rvlaue, на самом деле существует соответствующий общий 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

Это объясняет, почему у вас нет сбоя подстановки. Однако это невозможно сопоставить с вызовом std::cout << Foo(). Вот соответствующая часть ошибки компиляции:

/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)
    ^

Проблема здесь в том, что lhs может быть только ссылкой rvalue, но вы (очевидно) используете lvalue (т.е. std::cout) в вызове.