c++ - ¿Por qué no se pueden deducir instancias de plantillas en `std:: reference_wrapper`s?
templates implicit-conversion (2)
Supongamos que tengo algún objeto de tipo T
y quiero ponerlo en un envoltorio de referencia:
int a = 5, b = 7;
std::reference_wrapper<int> p(a), q(b); // or "auto p = std::ref(a)"
Ahora puedo decir fácilmente if (p < q)
, porque la envoltura de referencia tiene una conversión a su tipo envuelto. Todo está contento, y puedo procesar una colección de envoltorios de referencia como si fueran los objetos originales.
(Como muestra la pregunta que se vincula a continuación , esta puede ser una forma útil de generar una vista alternativa de una colección existente, que se puede reorganizar a voluntad sin incurrir en el costo de una copia completa, así como mantener la integridad de la actualización con la colección original. )
Sin embargo, con algunas clases esto no funciona:
std::string s1 = "hello", s2 = "world";
std::reference_wrapper<std::string> t1(s1), t2(s2);
return t1 < t2; // ERROR
Mi solución es definir un predicado como en esta respuesta *; pero mi pregunta es:
¿Por qué y cuándo se pueden aplicar los operadores a envoltorios de referencia y usar de manera transparente los operadores de los tipos envueltos? ¿Por qué falla para std::string
? ¿Qué tiene que ver con el hecho de que std::string
es una instancia de plantilla?
*) Actualización: A la luz de las respuestas, parece que usar std::less<T>()
es una solución general.
std::reference_wrapper
no tiene un operator<
, por lo que la única forma de hacerlo ref_wrapper<ref_wrapper
es a través del miembro ref_wrapper
:
operator T& () const noexcept;
Como saben, std::string
es:
typedef basic_string<char> string;
La declaración relevante para la string<string
es:
template<class charT, class traits, class Allocator>
bool operator< (const basic_string<charT,traits,Allocator>& lhs,
const basic_string<charT,traits,Allocator>& rhs) noexcept;
Para la string<string
esta plantilla de declaración de función está instanciada por la string
correspondiente = basic_string<charT,traits,Allocator>
que se resuelve en charT
= char
, etc.
Debido a que std::reference_wrapper
(o cualquiera de sus (cero) clases de bases) no pueden coincidir con basic_string<charT,traits,Allocator>
, la plantilla de declaración de función no puede ser instanciada en una declaración de función y no puede participar en la sobrecarga.
Lo que importa aquí es que no hay ningún prototipo operator< (string, string)
sin plantilla.
Código mínimo que muestra el problema.
template <typename T>
class Parametrized {};
template <typename T>
void f (Parametrized<T>);
Parametrized<int> p_i;
class Convertible {
public:
operator Parametrized<int> ();
};
Convertible c;
int main() {
f (p_i); // deduce template parameter (T = int)
f (c); // error: cannot instantiate template
}
Gives :
In function ''int main()'':
Line 18: error: no matching function for call to ''f(Convertible&)''
Citas estandar
14.8.2.1 Deducir argumentos de plantilla de una llamada de función [temp.deduct.call]
La deducción de los argumentos de la plantilla se realiza comparando cada tipo de parámetro de la plantilla de función (llámelo
P
) con el tipo del argumento correspondiente de la llamada (llámeloA
) como se describe a continuación.
(...)
En general, el proceso de deducción intenta encontrar valores de argumento de plantilla que harán que la
A
deducida sea idéntica aA
(después de que el tipoA
se transforme como se describe anteriormente). Sin embargo, hay tres casos que permiten una diferencia:
- Si la
P
original es un tipo de referencia, laA
deducida (es decir, el tipo al que hace referencia la referencia) puede ser más calificada como CV que laA
transformada.
Tenga en cuenta que este es el caso con std::string()<std::string()
.
- El
A
transformado puede ser otro puntero o puntero al tipo de miembro que se puede convertir alA
deducido mediante una conversión de calificación (4.4).
Ver comentario a continuación.
- Si
P
es una clase yP
tiene la forma simple-template-id , entonces laA
transformada puede ser una clase derivada de laA
deducida.
Comentario
Esto implica que en este párrafo:
14.8.1 Especificación de argumento de plantilla explícita [temp.arg.explicit] / 6
Las conversiones implícitas (Cláusula 4) se realizarán en un argumento de función para convertirlo al tipo del parámetro de función correspondiente si el tipo de parámetro no contiene parámetros de plantilla que participan en la deducción de argumento de plantilla.
El if no debe tomarse como un if y only if , ya que contradeciría directamente el texto citado anteriormente.
Edición: moviendo mis conjeturas al final, aquí viene el texto normativo por qué esto no funcionará. TL; versión DR:
No se permiten conversiones si el parámetro de función contiene un parámetro de plantilla deducido.
§14.8.3 [temp.over] p1
[...] Cuando se escribe una llamada a ese nombre (utilizando explícita o implícitamente la notación del operador), la deducción de los argumentos de la plantilla (14.8.2) y la verificación de los argumentos de la plantilla explícita (14.3) se realizan para cada plantilla de función para encontrar los valores de los argumentos de la plantilla (si existen) que se pueden usar con esa plantilla de función para crear una instancia de la especialización de la plantilla de función que se puede invocar con los argumentos de la llamada.
§14.8.2.1 [temp.deduct.call] p4
[...] [ Nota: como se especifica en 14.8.1, las conversiones implícitas se realizarán en un argumento de función para convertirlo al tipo del parámetro de función correspondiente si el parámetro no contiene parámetros de plantilla que participan en la deducción de argumento de plantilla . [...] —final de nota ]
§14.8.1 [temp.arg.explicit] p6
Las conversiones implícitas (Cláusula 4) se realizarán en un argumento de función para convertirlo al tipo del parámetro de función correspondiente si el tipo de parámetro no contiene parámetros de plantilla que participan en la deducción de argumento de plantilla. [ Nota: los parámetros de la plantilla no participan en la deducción de argumentos de la plantilla si se especifican explícitamente. [...] —final de nota ]
Como std::basic_string
depende de los parámetros de plantilla deducidos ( CharT
, Traits
), no se permiten conversiones.
Esto es una especie de problema de huevo y gallina. Para deducir el argumento de la plantilla, necesita una instancia real de std::basic_string
. Para convertir al tipo envuelto, se necesita un objetivo de conversión. Ese objetivo tiene que ser un tipo real, que no es una plantilla de clase. El compilador tendría que probar todas las posibles instancias de std::basic_string
contra el operador de conversión o algo así, lo cual es imposible.
Supongamos el siguiente testcase mínimo:
#include <functional>
template<class T>
struct foo{
int value;
};
template<class T>
bool operator<(foo<T> const& lhs, foo<T> const& rhs){
return lhs.value < rhs.value;
}
// comment this out to get a deduction failure
bool operator<(foo<int> const& lhs, foo<int> const& rhs){
return lhs.value < rhs.value;
}
int main(){
foo<int> f1 = { 1 }, f2 = { 2 };
auto ref1 = std::ref(f1), ref2 = std::ref(f2);
ref1 < ref2;
}
Si no proporcionamos la sobrecarga para una instanciación en int
, la deducción falla. Si proporcionamos esa sobrecarga, es algo con lo que el compilador puede probar con la conversión permitida definida por el usuario ( foo<int> const&
siendo el objetivo de conversión). Dado que la conversión coincide en este caso, la resolución de sobrecarga se realiza correctamente y recibimos nuestra llamada de función.