¿Por qué C ++ no puede deducir T en una llamada a Foo <T> :: Foo (T &&)?

9

Dada la siguiente estructura de plantilla:

template<typename T>
struct Foo {
    Foo(T&&) {}
};

Esto compila, y Tse deduce que es int:

auto f = Foo(2);

Pero esto no se compila: https://godbolt.org/z/hAA9TE

int x = 2;
auto f = Foo(x);

/*
<source>:12:15: error: no viable constructor or deduction guide for deduction of template arguments of 'Foo'
    auto f = Foo(x);
             ^

<source>:7:5: note: candidate function [with T = int] not viable: no known conversion from 'int' to 'int &&' for 1st argument
    Foo(T&&) {}
    ^
*/

Sin embargo, Foo<int&>(x)es aceptado.

Pero cuando agrego una guía de deducción definida por el usuario aparentemente redundante, funciona:

template<typename T>
Foo(T&&) -> Foo<T>;

¿Por qué no se Tpuede deducir int&sin una guía de deducción definida por el usuario?

jtbandes
fuente
Esa pregunta parece ser sobre tipos de plantillas comoFoo<T<A>>
jtbandes

Respuestas:

5

Creo que la confusión aquí surge porque hay una excepción específica para las guías de deducción sintetizadas con respecto al reenvío de referencias.

Es cierto que la función candidata para el propósito de la deducción de argumentos de plantilla generada a partir del constructor y la generada a partir de la guía de deducción definida por el usuario se ven exactamente iguales, es decir:

template<typename T>
auto f(T&&) -> Foo<T>;

pero para el generado por el constructor, T&&es una referencia de valor simple, mientras que es una referencia de reenvío en el caso definido por el usuario. Esto se especifica en [temp.deduct.call] / 3 del estándar C ++ 17 (borrador N4659, enfatice el mío):

Una referencia de reenvío es una referencia de valor r a un parámetro de plantilla no calificado cv que no representa un parámetro de plantilla de una plantilla de clase (durante la deducción de argumento de plantilla de clase ([over.match.class.deduct])).

Por lo tanto, el candidato sintetizado a partir del constructor de la clase no deducirá Tcomo si fuera una referencia de reenvío (que podría deducirse Tcomo una referencia de valor de l, por lo que T&&también es una referencia de valor de l), sino que solo deducirá Tcomo no referencia, por lo que T&&siempre es Una referencia de valor.

nuez
fuente
1
Gracias por la respuesta clara y concisa. ¿Sabe por qué hay una excepción para los parámetros de plantilla de clase en las reglas para reenviar referencias?
jtbandes
2
@jtbandes Esto parece haber cambiado como resultado de un comentario del organismo nacional de EE . UU . , véase el documento p0512r0 . Sin embargo, no pude encontrar el comentario. Supongo que la razón es que si escribe un constructor que toma una referencia de valor, generalmente espera que funcione de la misma manera si especifica ao Foo<int>(...)simplemente Foo(...), lo que no es el caso con el reenvío de referencias (que podría deducirse en su Foo<int&>lugar.
walnut
6

El problema aquí es que, puesto que la clase está en templated T, en el constructor Foo(T&&)que estamos no realizando el tipo de deducción; Siempre tenemos una referencia de valor r. Es decir, el constructor para Foorealmente se ve así:

Foo(int&&)

Foo(2)funciona porque 2es un prvalue.

Foo(x)no lo hace porque xes un valor que no se puede unir int&&. Podrías hacer std::move(x)para lanzarlo al tipo apropiado ( demo )

Foo<int&>(x)funciona bien porque el constructor se Foo(int&)debe a reglas de colapso de referencia; inicialmente es lo Foo((int&)&&)que colapsa Foo(int&)según el estándar.

Con respecto a su guía de deducción "redundante": Inicialmente hay una guía de deducción de plantilla predeterminada para el código que básicamente actúa como una función auxiliar de esta manera:

template<typename T>
struct Foo {
    Foo(T&&) {}
};

template<typename T>
Foo<T> MakeFoo(std::add_rvalue_reference_t<T> value)
{
   return Foo<T>(std::move(value));
}

//... 
auto f = MakeFoo(x);

Esto se debe a que el estándar dicta que este método de plantilla (ficticio) tiene los mismos parámetros de plantilla que la clase (Just T) seguido de cualquier parámetro de plantilla como el constructor (ninguno en este caso; el constructor no tiene plantilla). Entonces, los tipos de los parámetros de la función son los mismos que los del constructor. En nuestro caso, después de crear instancias Foo<int>, el constructor se ve como Foo(int&&)una referencia de valor en otras palabras. De ahí el uso de lo add_rvalue_reference_tanterior.

Obviamente esto no funciona.

Cuando agregó su guía de deducción "redundante":

template<typename T>
Foo(T&&) -> Foo<T>;

Permitieron que el compilador distinguir que, a pesar de cualquier tipo de referencia correspondiente a Ten el constructor ( int&, const int&o int&&etc.), que pretendía el tipo inferido para la clase de ser sin referencia (justo T). Esto se debe a que de repente estamos realizando inferencia de tipos.

Ahora generamos otra función auxiliar (ficticia) que se ve así:

template<class U>
Foo<U> MakeFoo(U&& u)
{
   return Foo<U>(std::forward<U>(u));
}

// ...
auto f = MakeFoo(x);

(Nuestras llamadas al constructor se redirigen a la función auxiliar con el propósito de deducir el argumento de la plantilla de clase, por lo que se Foo(x)convierte en MakeFoo(x)).

Esto permite U&&volverse int&y Tvolverse simplementeint

AndyG
fuente
El segundo nivel de plantillas no parece necesario; ¿Qué valor aporta? ¿Puede proporcionar un enlace a alguna documentación que aclare por qué T&& siempre se trata como una referencia de valor aquí?
jtbandes
1
Pero si T aún no se ha deducido, ¿qué significa "cualquier tipo convertible a T"?
jtbandes
1
Es rápido para ofrecer una solución, pero ¿puede centrarse más en la explicación de la razón por la que no funciona a menos que la modifique de alguna manera? ¿Por qué no funciona como está? " xes un valor al que no se puede unir int&&", pero alguien que no entienda quedará perplejo y Foo<int&>(x)podría funcionar, pero no se descubrió automáticamente. Creo que todos queremos una comprensión más profunda de por qué.
Wyck
2
@Wyck: He actualizado la publicación para centrarme más en el por qué.
AndyG
3
@Wyck La verdadera razón es muy simple: el estándar desactivó específicamente la semántica de reenvío perfecto en las guías de deducción sintetizadas para evitar sorpresas.
LF