¿Por qué no inferir el parámetro de plantilla del constructor?

102

mi pregunta de hoy es bastante simple: ¿por qué el compilador no puede inferir parámetros de plantilla de constructores de clases, tanto como puede hacerlo a partir de parámetros de función? Por ejemplo, ¿por qué no puede ser válido el siguiente código?

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 lo es? ¿Permitir esto crearía grandes agujeros sintácticos? ¿Hay alguna instancia en la que uno no quiera esta funcionalidad (donde inferir un tipo causaría problemas)? Solo estoy tratando de comprender la lógica detrás de permitir la inferencia de plantillas para funciones, pero no para clases construidas adecuadamente.

GRB
fuente
Invitaría a alguien (lo hago, pero no ahora mismo), para compilar la respuesta de Drahakar y Pitis (al menos) como buenos contraejemplos de por qué no puede funcionar
jpinto3912
2
También tenga en cuenta que esto se template<class T> Variable<T> make_Variable(T&& p) {return Variable<T>(std::forward<T>(p));}
soluciona
3
Puede obtener lo que desee var = Variable <decltype (n)> (n);
QuentinUK
18
¡C ++ 17 lo permitirá! Esta propuesta fue aceptada: open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0091r0.html
underscore_d
1
@underscore_d ¡Excelente! ¡Ya era hora! Me pareció natural que así fuera como debería funcionar, y la fuente de irritación de que no fuera así.
amdn el

Respuestas:

46

Creo que no es válido porque el constructor no siempre es el único punto de entrada de la clase (estoy hablando de constructor de copia y operador =). Entonces, suponga que está usando su clase así:

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 los argumentos del constructor.

Ejemplos:

std::pair p(2, 4.5);
std::tuple t(4, 3, 2.5);

Papel aceptado .

Drahakar
fuente
8
Este es realmente un gran punto que nunca consideré. No veo ninguna forma de evitar el hecho de que el puntero tendría que ser de tipo específico (es decir, tendría que ser MyClass <string> * pm). Si ese es el caso, entonces todo lo que terminaría haciendo es evitar especificar el tipo en la instanciación; unos pocos caracteres de trabajo extra (y solo si el objeto está hecho en la pila, no en el montón, como se indica arriba). Siempre sospeché que la inferencia de clases puede abrir una lata sintáctica de gusanos, y creo que puede ser esto.
GRB
2
No veo muy bien cómo permitir la inferencia de parámetros de plantilla desde constructores requeriría permitir declaraciones no especializadas sin llamadas de constructor, como en su segunda línea. Es decir, MyClass *pmaquí no sería válido por la misma razón que una función declarada template <typename T> void foo();no se puede llamar sin una especialización explícita.
Kyle Strand
3
@KyleStrand Sí, al decir 'los argumentos de la plantilla de clase no se pueden deducir de sus constructores porque [ejemplo que no usa ningún constructor] ', esta respuesta es completamente irrelevante. Realmente no puedo creer que fue aceptado, alcanzó +29, tomó 6 años para que alguien notara el problema evidente y se sentó sin un solo voto negativo durante 7 años. ¿Nadie más piensa mientras lee, o ...?
underscore_d
1
@underscore_d Me gusta cómo, tal como está actualmente, esta respuesta dice "podría haber algunos problemas con esta propuesta; no estoy seguro de si lo que acabo de decir tiene sentido (!), no dude en comentar (!!); y oh, por cierto, así es exactamente como funcionará C ++ 17 ".
Kyle Strand
1
@KyleStrand Ah, sí, ese es otro problema, que noté pero olvidé mencionar entre todas las demás cosas divertidas. La edición sobre C ++ 17 no fue realizada por el OP ... y la OMI no debería haber sido aprobada, sino publicada como una nueva respuesta: habría sido rechazada ya que 'cambia el significado de la publicación' incluso si la publicación hubiera No ha sido insignificante para empezar ... No sabía que editar en secciones completamente nuevas era un juego limpio y ciertamente se han rechazado ediciones menos drásticas, pero supongo que esa es la suerte del sorteo en términos de los revisores que obtienes.
underscore_d
27

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 con nombre. Entonces, no solo hace lo que quieres, sino que casi se llama lo que quieres: el compilador está infiriendo el parámetro de plantilla del constructor (nombrado).

NB: cualquier compilador razonable optimizará el objeto temporal cuando escriba algo como

auto v = make_variable(instance);
Lionel
fuente
6
Quiero señalar que no es particularmente útil hacer que la función sea miembro estático en tal caso porque para eso tendría que especificar un argumento de plantilla para que una clase lo llame de todos modos, por lo que no tendría sentido deducirlo.
Predelnik
3
Y aún mejor en C ++ 11, puede hacerlo auto v = make_variable(instance)para que no tenga que especificar el tipo
Claudiu
1
Sí, jajaja ante la idea de declarar la función make como staticmiembro ... piensa en eso por solo un segundo. Aparte de eso: las funciones de creación gratuitas fueron de hecho la solución, pero es un montón de repetición redundante, que mientras lo escribe, sabe que no debería tener que hacerlo porque el compilador tiene acceso a toda la información que está repitiendo. .. y afortunadamente C ++ 17 lo canoniza.
underscore_d
21

En la era ilustrada de 2016, con dos nuevos estándares en nuestro haber desde que se hizo esta pregunta y uno nuevo a la vuelta de la esquina, lo crucial que debe saber es que los compiladores que admiten 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) está el documento que detalla los cambios relevantes al estándar.

Abordar preocupaciones de otras respuestas

La respuesta mejor calificada actual

Esta respuesta señala que el "constructor de copias y operator=" no conocerían las especializaciones de plantilla correctas.

Esto es una tontería, porque el constructor de copia estándar y operator= solo existe 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 razón para MyClass *pmser 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 declarar un puntero de tipo MyClass. Aquí hay una forma posible de corregir el ejemplo:

MyClass m(string("blah blah blah"));
decltype(m) *pm;               // uses type inference!
*pm = m;

Aquí, yapm es del tipo correcto, por lo que la inferencia es trivial. Además, es imposible mezclar tipos accidentalmente al llamar al constructor de copias:

MyClass m(string("blah blah blah"));
auto pm = &(MyClass(m));

Aquí, pmhabrá un puntero a una copia de m. Aquí, MyClassse está construyendo una copia a partir de lo mque es de tipo MyClass<string>(y no del tipo inexistente MyClass). Por lo tanto, en el punto donde pmse infiere 's tipo, no es información suficiente para saber que el tipo de plantilla de m, y por lo tanto el tipo de plantilla de pm, es 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 tiene plantilla:

MyClass(const MyClass&);

Aquí, el tipo de plantilla del argumento del constructor de copia coincide con el tipo de plantilla de la clase en general; es decir, cuando MyClass<string>se instancia, MyClass<string>::MyClass(const MyClass<string>&);se instancia con él, y cuando MyClass<int>se instancia, MyClass<int>::MyClass(const MyClass<int>&);se instancia. A menos que se especifique explícitamente o se declare un constructor con plantilla, no hay razón para que el compilador cree una instancia 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>luego dice:

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, en Variablesí mismo no es un nombre de tipo, aunque la nueva característica hace que parezca uno sintácticamente.

Pitiș luego pregunta qué pasaría si no se proporciona un constructor que permita la inferencia adecuada. La respuesta es que no se permite ninguna inferencia, porque la inferencia se activa mediante la llamada al constructor . Sin una llamada al constructor, no hay inferencia .

Esto es similar a preguntar qué versión de foose deduce aquí:

template <typename T> foo();
foo();

La respuesta es que este código es ilegal, por la razón indicada.

La respuesta de MSalter

Esta es, por lo que puedo decir, la única respuesta que plantea 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 aquí el constructor de tipo inferido o el constructor de copia ?

Al probar el código, podemos ver que el constructor de copia está seleccionado. Para ampliar 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 todavía no entiendo.

Tampoco estoy seguro de por qué la var4deducción es ilegal; el error del compilador de g ++ parece indicar que la declaración se está analizando como una declaración de función.

Kyle Strand
fuente
¡Qué gran respuesta detallada! var4es solo un caso del "análisis más molesto" (no relacionado con la deducción de arg de la plantilla). Solíamos usar paréntesis adicionales para esto, pero en estos días creo que usar tirantes para denotar de manera inequívoca la construcción es el consejo habitual.
Sumudu Fernando
@SumuduFernando ¡Gracias! ¿Quiere decir que Variable var4(Variable(num));se trata como una declaración de función? Si es así, ¿por qué es Variable(num)una especificación de parámetro válida?
Kyle Strand
@SumuduFernando No importa, no tenía idea de que esto fuera válido: coliru.stacked-crooked.com/a/98c36b8082660941
Kyle Strand
11

Aún 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>> ?
}
MSalters
fuente
Otro buen punto. Suponiendo que existe una Variable definida por el constructor de copias (Variable <obj> d), tendría que haber algún tipo de precedencia establecida.
GRB
1
O, alternativamente, haga que el compilador arroje nuevamente un error de parámetro de plantilla indefinido, como sugerí con respecto a la respuesta de Pitis. Sin embargo, si toma esa ruta, la cantidad de veces en que la inferencia puede ocurrir sin problemas (errores) es cada vez menor.
GRB
Este es en realidad un punto interesante y (como he señalado en mi respuesta) todavía no estoy seguro de cómo la propuesta aceptada de C ++ 17 resuelve esto.
Kyle Strand
9

Suponiendo que el compilador sea compatible con lo que solicitó. 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 en Variables y no proporciono un 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é debe hacer el compilador? ¿Usar la definición de clase de Variable genérica para deducir que es Variable, luego descubrir que Variable no proporciona un constructor de parámetros?

Cătălin Pitiș
fuente
1
Peor: ¿qué pasa si solo tiene Variable <int> :: Variable (float)? Ahora tiene dos formas de deducir la Variable (1f) y ninguna forma de deducir la Variable (1).
MSalters
Es un buen punto, pero podría superarse fácilmente con el casting: Variable v1 ((doble) 10)
jpinto3912
Estoy de acuerdo en que la legibilidad del código es un problema subjetivo, sin embargo, estoy 100% de acuerdo con lo que dice sobre la especialización de plantillas. La solución probablemente sería dar un error de parámetro de plantilla indefinido (una vez que el compilador mira la especialización <int> y no ve constructores válidos, dígale que no tiene idea de qué plantilla desea usar y que debe especificar explícitamente) pero Estoy de acuerdo en que no es una solución bonita. Agregaría esto como otro gran agujero sintáctico que debería ser tratado (pero podría resolverse si uno acepta las consecuencias).
GRB
4
@ jpinto3912 - te estás perdiendo el punto. El compilador tiene que instanciar TODAS las posibles Variables <T> para comprobar si CUALQUIER ctor Variable <T> :: Variable proporciona un ctor ambiguo. Deshacerse de la ambigüedad no es el problema: simplemente cree una instancia de Variable <double> usted mismo si eso es lo que desea. Es encontrar esa ambigüedad en primer lugar lo que lo hace imposible.
MSalters
6

Los estándares C ++ 03 y C ++ 11 no permiten la deducción de argumentos de plantilla de los parámetros pasados ​​al constructor.

Pero hay una propuesta para la "Deducción de parámetros de plantilla para constructores" para que pueda 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 http://www.open-std.org/jtc1/sc22/wg21/docs/ papers / 2015 / p0091r0.html

ChetS
fuente
La función se ha agregado a C ++ 17, pero no si "pronto" se aplica a un período de tiempo de 6 a 8 años. ;)
ChetS
2

Muchas clases no dependen de los parámetros del constructor. Solo hay unas pocas clases que tienen un solo constructor y se parametrizan en función de los tipos de este constructor.

Si realmente necesita una 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);
}
rlbond
fuente
1
Por supuesto, esta funcionalidad solo resultaría útil para algunas clases, pero lo mismo puede decirse de la inferencia de funciones. Tampoco todas las funciones con plantilla toman sus parámetros de la lista de argumentos, sin embargo, permitimos la inferencia para aquellas funciones que lo hacen.
GRB
1

La deducción de tipos se limita a las funciones de plantilla en C ++ actual, pero desde hace mucho tiempo se sabe que la deducción de tipos en otros contextos sería muy útil. Por lo tanto, C ++ 0x's auto.

Si bien exactamente lo que sugieres no será posible en C ++ 0x, lo siguiente muestra que puedes acercarte 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>
}
James Hopkin
fuente
0

Tiene razón, el compilador podría adivinar fácilmente, pero no está en el estándar o en C ++ 0x que yo sepa, por lo que tendrá que esperar al menos 10 años más (tasa de respuesta fija de los estándares ISO) antes de que los proveedores del compilador agreguen esta función

Robert Gould
fuente
Eso no es correcto con el próximo estándar, se introducirá una palabra clave automática. Eche un vistazo a la publicación de James Hopkins en este hilo. stackoverflow.com/questions/984394/… . Muestra cómo será posible en C ++ 0x.
ovanes
1
Solo para corregirme, la palabra clave auto también está presente en el estándar actual, pero con un propósito diferente.
ovanes
Parece que serán 8 años (desde el momento de esta respuesta) ... así que 10 años no fue una suposición incorrecta, ¡aunque ha habido dos estándares mientras tanto!
Kyle Strand
-1

Veamos 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 de 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 de tamaño predeterminado:

vector <string> v(100);

Aquí, si se usara inferencia:

vector v(100);

obtenemos un vector de ints, no de cadenas, ¡y presumiblemente no tiene el tamaño!

Por último, considere los constructores que toman varios parámetros, con "inferencia":

vector v( 100, foobar() );      // foobar is some class

¿Qué parámetro debería usarse para la inferencia? Necesitaríamos alguna forma de decirle al compilador que debería ser el segundo.

Con todos estos problemas para una clase tan simple como un vector, es fácil ver por qué no se usa la inferencia.


fuente
3
Creo que estás malinterpretando la idea. La inferencia de tipos para constructores solo se produciría SI el tipo de plantilla es parte del constructor. Suponga que el vector tiene la plantilla de definición de plantilla <typename T>. Su ejemplo no es un problema porque el constructor del vector se definiría como vector (tamaño int), no vector (tamaño T). Sólo en el caso del vector (tamaño T) se produciría alguna inferencia; en el primer ejemplo, el compilador daría un error diciendo que T no está definido. Esencialmente idéntico a cómo funciona la inferencia de plantilla de función.
GRB
Entonces, ¿solo se llevaría a cabo para los constructores que tienen un solo parámetro y donde ese parámetro es un tipo de parámetro de plantilla? Eso parece un número muy reducido de casos.
No es necesario que sea un solo parámetro. Por ejemplo, se podría tener un constructor de vector de vector (int size, T firstElement). Si una plantilla tiene varios parámetros (plantilla <typename T, typename U>), uno podría tener Holder :: Holder (T firstObject, U secondObject). Si una plantilla tiene varios parámetros pero el constructor solo toma uno de ellos, por ejemplo, Holder (U secondObject), entonces T siempre tendría que estar explícitamente indicado. Las reglas deberían ser lo más similares posible a la inferencia de la plantilla de funciones.
GRB
-2

Haciendo 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 varios miembros de Variable :: data.

Nick Dandoulakis
fuente
Eso no tendría sentido bajo ningún escenario. obj en términos de datos obj no está definido ya que esa clase ya no es una plantilla. Dicho código sería inválido de cualquier manera.
GRB
Quería el comportamiento del compilador que describe, así que descubro una manera de eludir esa restricción (en mi caso), que puede encontrar interesante, stackoverflow.com/questions/228620/garbage-collection-in-c-why/…
Nick Dandoulakis
-2

Consulte La deducción del argumento de la plantilla de C ++ para obtener más información al respecto.

Igor Krivokon
fuente
4
Leí este artículo antes y no parecía hablar mucho sobre lo que estoy diciendo. La única vez que el escritor parece hablar sobre la deducción de argumentos con respecto a las clases es cuando dice que no se puede hacer en la parte superior del artículo;) - si pudiera señalar las secciones que cree que son relevantes aunque yo ' Realmente lo agradecería.
GRB