tipos - plantillas multiples c++
¿Por qué no inferir el parámetro de la plantilla desde el constructor? (13)
Al hacer del ctor una plantilla, la variable puede tener solo una forma, pero varios ctors:
class Variable {
obj data; // let the compiler guess
public:
template<typename obj>
Variable(obj d)
{
data = d;
}
};
int main()
{
int num = 2;
Variable var(num); // Variable::data int?
float num2 = 2.0f;
Variable var2(num2); // Variable::data float?
return 0;
}
¿Ver? No podemos tener múltiples miembros de Variable :: data.
mi pregunta de hoy es bastante simple: ¿por qué el compilador no puede inferir los parámetros de la plantilla de los constructores de la clase, como puede hacerlo desde los parámetros de la función? Por ejemplo, ¿por qué el siguiente código no podría ser válido?
template<typename obj>
class Variable {
obj data;
public: Variable(obj d)
{
data = d;
}
};
int main()
{
int num = 2;
Variable var(num); //would be equivalent to Variable<int> var(num),
return 0; //but actually a compile error
}
Como digo, entiendo que esto no es válido, entonces mi pregunta es ¿por qué no? ¿Permitiría esto crear algún agujero sintáctico importante? ¿Hay alguna instancia en la que no se quiera esta funcionalidad (donde inferir un tipo podría causar problemas)? Solo trato de entender la lógica detrás de permitir la inferencia de plantillas para las funciones, pero no para las clases construidas adecuadamente.
Creo que no es válido porque el constructor no siempre es el único punto de entrada de la clase (estoy hablando de copy constructor y operator =). Entonces supongamos que estás usando tu clase de esta manera:
MyClass m(string s);
MyClass *pm;
*pm = m;
No estoy seguro de si sería tan obvio para el analizador saber qué tipo de plantilla es MyClass pm;
No estoy seguro de si lo que dije tiene sentido, pero siéntase libre de agregar algún comentario, esa es una pregunta interesante.
C ++ 17
Se acepta que C ++ 17 tendrá deducción de tipo de argumentos de constructor.
Ejemplos:
std::pair p(2, 4.5);
std::tuple t(4, 3, 2.5);
El estándar C ++ 03 y C ++ 11 no permite la deducción del argumento de la plantilla de los parámetros pasados al constuructor.
Pero hay una propuesta para "Deducción de parámetros de plantilla para constructores", por lo que puede obtener lo que está pidiendo pronto. Editar: de hecho, esta característica ha sido confirmada para C ++ 17.
Ver: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3602.html y open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0091r0.html
En la era ilustrada de 2016, con dos nuevos estándares en nuestro haber desde que se hizo esta pregunta y una nueva a la vuelta de la esquina, lo más importante es saber que los compiladores que soportan el estándar C ++ 17 compilarán su código tal como está .
Deducción de argumento de plantilla para plantillas de clase en C ++ 17
Aquí (cortesía de una edición de Olzhas Zhumabek de la respuesta aceptada) se encuentra el documento que detalla los cambios relevantes al estándar.
Abordar las preocupaciones de otras respuestas
La respuesta actual mejor calificada
Esta respuesta señala que "copy constructor and operator=
" no sabría las especializaciones de plantillas correctas.
Esto no tiene sentido, porque el constructor de copias estándar y el operator=
solo existen para un tipo de plantilla conocido :
template <typename T>
class MyClass {
MyClass(const MyClass&) =default;
... etc...
};
// usage example modified from the answer
MyClass m(string("blah blah blah"));
MyClass *pm; // WHAT IS THIS?
*pm = m;
Aquí, como señalé en los comentarios, no hay ninguna razón para que MyClass *pm
sea una declaración legal con o sin la nueva forma de inferencia: MyClass
no es un tipo (es una plantilla), por lo que no tiene sentido declara un puntero de tipo MyClass
. Aquí hay una forma posible de corregir el ejemplo:
MyClassstring m("blah blah blah"));
decltype(m) *pm; // uses type inference!
*pm = m;
Aquí, pm
ya es del tipo correcto, por lo que la inferencia es trivial. Además, es imposible mezclar accidentalmente tipos al llamar al constructor de copias:
MyClass m(string("blah blah blah"));
auto pm = &(MyClass(m));
Aquí, pm
será un puntero a una copia de m
. Aquí, MyClass
está siendo copiado de m
que es del tipo MyClass<string>
(y no del tipo inexistente MyClass
). Por lo tanto, en el punto donde se deduce el tipo de pm
, hay suficiente información para saber que el tipo de plantilla de m
, y por lo tanto el tipo de plantilla de pm
, es una string
.
Además, lo siguiente siempre generará un error de compilación :
MyClass s(string("blah blah blah"));
MyClass i(3);
i = s;
Esto se debe a que la declaración del constructor de copia no está templada:
MyClass(const MyClass&);
Aquí, el tipo de plantilla del argumento copy-constructor coincide con el tipo de plantilla de la clase en general; es decir, cuando MyClass<string>
se crea una instancia, MyClass<string>::MyClass(const MyClass<string>&);
se crea una instancia con él, y cuando se MyClass<int>
instancia MyClass<int>::MyClass(const MyClass<int>&);
, MyClass<int>::MyClass(const MyClass<int>&);
es instanciado. A menos que se especifique explícitamente o se declare un constructor con plantilla, no hay ninguna razón para que el compilador MyClass<int>::MyClass(const MyClass<string>&);
instancia de MyClass<int>::MyClass(const MyClass<string>&);
, lo que obviamente sería inapropiado.
La respuesta de Cătălin Pitiş
Pitiş da un ejemplo deduciendo Variable<int>
y Variable<double>
, y luego declara:
Tengo el mismo nombre de tipo (Variable) en el código para dos tipos diferentes (Variable y Variable). Desde mi punto de vista subjetivo, afecta bastante la legibilidad del código.
Como se señaló en el ejemplo anterior, Variable
sí no es un nombre de tipo, aunque la nueva característica lo hace parecer sintácticamente.
Pitiş luego pregunta qué pasaría si no se proporciona un constructor que permita la inferencia apropiada. La respuesta es que no se permite ninguna inferencia, porque la inferencia se desencadena por la llamada del constructor . Sin una llamada de constructor, no hay inferencia .
Esto es similar a preguntar qué versión de foo
se deduce aquí:
template <typename T> foo();
foo();
La respuesta es que este código es ilegal, por la razón indicada.
Respuesta de MSalter
Esto es, hasta donde puedo decir, la única respuesta para plantear una preocupación legítima sobre la función propuesta.
El ejemplo es:
Variable var(num); // If equivalent to Variable<int> var(num),
Variable var2(var); //Variable<int> or Variable<Variable<int>> ?
La pregunta clave es, ¿el compilador selecciona el constructor de tipo inferido aquí o el constructor de copia ?
Probando el código, podemos ver que el constructor de copia está seleccionado. Para expandir el ejemplo :
Variable var(num); // infering ctor
Variable var2(var); // copy ctor
Variable var3(move(var)); // move ctor
// Variable var4(Variable(num)); // compiler error
No estoy seguro de cómo la propuesta y la nueva versión del estándar especifican esto; parece estar determinado por "guías de deducción", que son un nuevo estándar que aún no entiendo.
Tampoco estoy seguro de por qué la deducción de var4
es ilegal; el error del compilador de g ++ parece indicar que la declaración se está analizando como una declaración de función.
La deducción de tipos está limitada a las funciones de plantilla en C ++ actual, pero hace tiempo que se sabe que la deducción de tipo en otros contextos sería muy útil. De ahí el auto
C ++ 0x.
Si bien lo que sugiere no será posible en C ++ 0x, lo siguiente muestra que puede acercarse bastante:
template <class X>
Variable<typename std::remove_reference<X>::type> MakeVariable(X&& x)
{
// remove reference required for the case that x is an lvalue
return Variable<typename std::remove_reference<X>::type>(std::forward(x));
}
void test()
{
auto v = MakeVariable(2); // v is of type Variable<int>
}
Lo que estás tratando de lograr se llama borrado de tipo. Eche un vistazo a boost :: any y su implementación como funciona.
Ahora a la pregunta, por qué no funciona. Como boost :: any demuestra que es posible implementarlo. Y funciona, pero tu problema sería el despacho, que tipo está realmente adentro. Contrariamente al enfoque de plantilla, se le pedirá que realice despacho en el tiempo de ejecución de la aplicación, ya que el tipo contenido se borrará. Hay 2 posibilidades para manejar eso: el patrón de visitante y la implementación de conversión personalizada (que arroja una excepción si está tratando de convertir al tipo incorrecto). En ambos casos, mueve la seguridad del tipo de tiempo de compilación al tiempo de ejecución y omite las comprobaciones de tipo del compilador.
Otro enfoque es introducir el tipo de Variante: Variante Boost
La variante funciona de forma diferente como boost :: any y permite almacenar solo un número limitado de tipos. Esto introduce mayor seguridad de tipo, ya que realmente limita el conjunto de tipos esperados. Hay un buen artículo escrito por Andrey Alexandrescu en ddj, sobre cómo implementar dicha variante: Parte 1 , Parte 2 , Parte 3
Su implementación es un poco más compleja que la de boost :: any, pero ofrece mayor seguridad de tipo y no permite a los usuarios incluir cualquier tipo posible en la variante, con excepción de los que se declaran explícitamente.
Como dije, puede implementarse en C ++, pero requiere un conocimiento profundo del lenguaje y un buen diseño de la interfaz, de modo que los usuarios de esa clase no manejen las manzanas como melocotones.
Por favor, recuerde las palabras de Henry Spencer: si le miente al compilador, obtendrá su venganza.
Saludos,
Ovanes
Idea de implementación (! No probado!)
class any_type
{
class any_container
{
public:
virtual ~any_container(){}
virtual void* pointer()=0;
virtual type_info const& get_type()const=0;
};
template<class T>
class any_container_impl
{
T t_;
public:
any_container_impl(T const& t)
: t_(t)
{}
virtual ~any_container_impl(){}
virtual void* pointer()
{
return &t_;
}
virtual type_info const& get_type()const
{
return typeid(T);
}
};
std::auto_ptr<any_container> content_;
public:
template<class T>
any_type(T const& t)
: content_(new any_container_impl<T>(t))
{}
template<class T>
T* cast_to_ptr()
{
if(typeid(T)!=content_->get_type())
return NULL;
return reinterpret_cast<T*>(content_->pointer());
}
template<class T>
T& cast_to_ref()
{
T* ptr = cast_to_ptr<T>();
if(!ptr)
throw std::logic_error("wrong type");
return *ptr;
}
};
Miremos el problema con referencia a una clase con la que todos deberían estar familiarizados: std :: vector.
En primer lugar, un uso muy común del vector es usar el constructor que no toma parámetros:
vector <int> v;
En este caso, obviamente no se puede realizar ninguna inferencia.
Un segundo uso común es crear un vector pre-clasificado:
vector <string> v(100);
Aquí, si se utilizó la inferencia:
vector v(100);
obtenemos un vector de enteros, no de cadenas, ¡y presumiblemente no tiene el tamaño!
Por último, considere los constructores que toman múltiples parámetros, con "inferencia":
vector v( 100, foobar() ); // foobar is some class
¿Qué parámetro debería usarse para la inferencia? Necesitaríamos alguna manera de decirle al compilador que debería ser el segundo.
Con todos estos problemas para una clase tan simple como vector, es fácil ver por qué no se usa la inferencia.
Muchas clases no dependen de los parámetros del constructor. Hay solo unas pocas clases que tienen solo un constructor y parametrizan en función de los tipos de este constructor.
Si realmente necesita inferencia de plantilla, use una función auxiliar:
template<typename obj>
class Variable
{
obj data;
public:
Variable(obj d)
: data(d)
{ }
};
template<typename obj>
inline Variable<obj> makeVariable(const obj& d)
{
return Variable<obj>(d);
}
No puede hacer lo que pide por razones que otras personas han abordado, pero puede hacer esto:
template<typename T>
class Variable {
public: Variable(T d) {}
};
template<typename T>
Variable<T> make_variable(T instance) {
return Variable<T>(instance);
}
que para todos los propósitos y propósitos es lo mismo que pides. Si te encanta la encapsulación, puedes hacer que make_variable sea una función miembro estática. Eso es lo que la gente llama constructor nombrado. Así que no solo hace lo que quiere, sino que casi se llama lo que quiere: el compilador está infiriendo el parámetro de la plantilla del constructor (nombrado).
NB: cualquier compilador razonable optimizará el objeto temporal cuando escriba algo así como
Variable<T> v = make_variable(instance);
Supongamos que el compilador admite lo que usted pidió. Entonces este código es válido:
Variable v1( 10); // Variable<int>
// Some code here
Variable v2( 20.4); // Variable<double>
Ahora, tengo el mismo nombre de tipo (Variable) en el código para dos tipos diferentes (Variable y Variable). Desde mi punto de vista subjetivo, afecta bastante la legibilidad del código. Tener el mismo nombre de tipo para dos tipos diferentes en el mismo espacio de nombres me parece engañoso.
Actualización posterior: Otra cosa a considerar: especialización de plantilla parcial (o completa).
¿Qué pasa si me especializo Variable y no proporciono ningún constructor como usted espera?
Entonces yo tendría:
template<>
class Variable<int>
{
// Provide default constructor only.
};
Entonces tengo el código:
Variable v( 10);
¿Qué debería hacer el compilador? Use la definición de clase variable genérica para deducir que es variable, luego descubra que la variable no proporciona un constructor de parámetro.
Tiene razón el compilador podría adivinar fácilmente, pero no está en el estándar o C ++ 0x por lo que sé, así que tendrá que esperar al menos 10 años más (estándares ISO velocidad de respuesta fija) antes de que los proveedores de compilación agreguen esta característica
Todavía falta: hace que el siguiente código sea bastante ambiguo:
int main()
{
int num = 2;
Variable var(num); // If equivalent to Variable<int> var(num),
Variable var2(var); //Variable<int> or Variable<Variable<int>> ?
}
Vea la Deducción del argumento de la plantilla C ++ para más información sobre esto.