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
) в вызове.