c++ - Expresión declval(para SFINAE) con std:: ostream
(3)
Estoy tratando de crear una clase de rasgos de tipo para determinar si un tipo T
particular puede transmitirse a través del operador <<
de un std::ostream
. Estoy usando una técnica SFINAE directa.
En última instancia, la expresión que intento evaluar para el fallo de sustitución es:
decltype(std::declval<std::ostream>() << std::declval<T>()) ;
Mi expectativa es que, dada una instancia t
de tipo T
y una std::ostream
instance os
, si la expresión os << t
está mal formada, debería producirse un fallo de sustitución.
Pero aparentemente el fallo de sustitución nunca se produce aquí, independientemente del tipo T
E incluso si simplemente declaro un typedef
usando la expresión decltype
anterior, fuera del contexto de SFINAE, se compila felizmente, incluso si T
no se puede usar con std::ostream
.
Por ejemplo:
struct Foo { };
int main()
{
// This compiles fine using GCC 4.9.2
//
typedef decltype(
std::declval<std::ostream>() << std::declval<Foo>()
) foo_type;
}
Lo anterior compilará bien usando GCC 4.9.2, que no es lo que esperaba ya que el operador <<
no está sobrecargado para trabajar con un tipo Foo
. Y claro, si digo:
std::cout << Foo();
... me sale un error de compilación Entonces, ¿por qué la expresión decltype
arriba incluso se compila?
C ++ 11 agregó el siguiente operator<<
sobrecarga:
template< class CharT, class Traits, class T >
basic_ostream< CharT, Traits >& operator<<( basic_ostream<CharT,Traits>&& os,
const T& value );
Esto reenvía a los operadores de inserción estándar, que no pueden enlazar las referencias de valor a std::ostream
s porque toman referencias no constantes. Dado que std::declval<std::ostream>
devuelve std::ostream&&
, se selecciona esta sobrecarga, luego debido a la interfaz muy permisiva (es decir, esto no es SFINAEd si no hay un operador de inserción subyacente válido), su especificador de tipo de decltype
trabajos.
La solución simple es usar std::declval<std::ostream&>()
. Esto devolverá un std::ostream&
, por lo que la sobrecarga de la plantilla no será seleccionada por su especificador de tipo de decltype
y se decltype
una sobrecarga del operador de inserción normal para que compile:
typedef decltype(
std::declval<std::ostream&>() << std::declval<Foo>()
) foo_type;
Clang produce esto:
main.cpp:8:39: error: invalid operands to binary expression (''std::basic_ostream<char>'' and ''Foo'')
std::declval<std::ostream&>() << std::declval<Foo>()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ^ ~~~~~~~~~~~~~~~~~~~
Aquí hay un ejemplo más simple que exhibe el mismo problema:
#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;
}
Aquí están las citas de estándares relevantes (N4140):
La declaración debe ser instanciada porque la resolución de sobrecarga está involucrada:
[temp.inst]/10:
Si se utiliza una plantilla de función o una especialización de plantilla de función miembro de una manera que implica resolución de sobrecarga, se crea una instancia de la declaración de la especialización (14.8.3).
Solo la declaración necesita ser instanciada:
[temp.over]/5:
solo se necesita la firma de una especialización de plantilla de función para ingresar la especialización en un conjunto de funciones candidatas. Por lo tanto, solo se necesita la declaración de plantilla de función para resolver una llamada para la cual una especialización de plantilla es un candidato.
Y la implementación no tiene permitido instanciar el cuerpo de la función:
[temp.inst]/11:
Una implementación no creará una instancia implícitamente de una plantilla de función, una plantilla de variable, una plantilla de miembro, una función de miembro no virtual, una clase de miembro o un miembro de datos estáticos de una plantilla de clase que no requiera instanciación
Realmente no responde por qué sucede esto, pero si reemplaza con std::stream&
siguiente manera:
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 {};
parece funcionar.
Si observa el ostream
archivo de ostream
, encontrará que dado que std::declval
produce referencias de rvlaue, en realidad hay un operator<<
genérico que corresponde 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
Esto explica por qué no se produce ningún fallo en la subestación. Sin embargo, esto no puede compararse con la llamada std::cout << Foo()
. Aquí está la parte relevante del error de compilación:
/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)
^
El problema aquí es que lhs solo puede ser una referencia rvalue, pero (obviamente) estás usando un lvalue (es decir, std::cout
) en la llamada.