Considere este programa bastante inútil:
#include <iostream>
int main(int argc, char* argv[]) {
int a = 5;
auto it = [&](auto self) {
return [&](auto b) {
std::cout << (a + b) << std::endl;
return self(self);
};
};
it(it)(4)(6)(42)(77)(999);
}
Básicamente estamos tratando de hacer una lambda que regrese.
- MSVC compila el programa y se ejecuta
- gcc compila el programa, y se daña por defecto
- clang rechaza el programa con un mensaje:
error: function 'operator()<(lambda at lam.cpp:6:13)>' with deduced return type cannot be used before it is defined
¿Qué compilador es el correcto? ¿Existe una violación de restricción estática, UB o ninguna de las dos?
Actualizar esta ligera modificación es aceptada por clang:
auto it = [&](auto& self, auto b) {
std::cout << (a + b) << std::endl;
return [&](auto p) { return self(self,p); };
};
it(it,4)(6)(42)(77)(999);
Actualización 2 : entiendo cómo escribir un functor que se devuelve solo, o cómo usar el combinador Y, para lograr esto. Esta es más una pregunta de abogado de idiomas.
Actualización 3 : la pregunta no es si es legal que una lambda regrese en general, sino la legalidad de esta forma específica de hacerlo.
Pregunta relacionada: C ++ lambda regresando a sí mismo .
auto& self
que elimina el problema de referencia pendiente.Respuestas:
El programa está mal formado (clang es correcto) según [dcl.spec.auto] / 9 :
Básicamente, la deducción del tipo de retorno del lambda interno depende de sí misma (la entidad que se nombra aquí es el operador de llamada), por lo que debe proporcionar explícitamente un tipo de retorno. En este caso particular, eso es imposible, porque necesita el tipo de lambda interior pero no puede nombrarlo. Pero hay otros casos en los que intentar forzar lambdas recursivas como esta puede funcionar.
Incluso sin eso, tienes una referencia pendiente .
Permítanme elaborar un poco más, después de discutir con alguien mucho más inteligente (es decir, TC). Hay una diferencia importante entre el código original (ligeramente reducido) y la nueva versión propuesta (igualmente reducida):
Y es que la expresión interna
self(self)
no es dependiente def1
, sino queself(self, p)
es dependiente def2
. Cuando las expresiones no son dependientes, se pueden usar ... con entusiasmo ( [temp.res] / 8 , p. Ej., ¿Cómostatic_assert(false)
es un error grave independientemente de si la plantilla en la que se encuentra está instanciada o no?).Porque
f1
un compilador (como, por ejemplo, clang) puede intentar crear una instancia con entusiasmo. Usted sabe el tipo deducido de la lambda externa una vez que llega a eso;
en el punto#2
anterior (es el tipo de la lambda interna), pero estamos tratando de usarlo antes que eso (piense en ello como en el punto#1
): estamos tratando usarlo mientras todavía estamos analizando el lambda interno, antes de saber qué tipo es realmente. Eso va en contra de dcl.spec.auto/9.Sin embargo, para
f2
, no podemos tratar de crear instancias con entusiasmo, porque depende. Solo podemos crear instancias en el punto de uso, momento en el cual sabemos todo.Para realmente hacer algo como esto, necesitas un combinador y . La implementación del documento:
Y lo que quieres es:
fuente
Editar : Parece haber cierta controversia sobre si esta construcción es estrictamente válida según la especificación de C ++. La opinión predominante parece ser que no es válida. Vea las otras respuestas para una discusión más completa. El resto de esta respuesta se aplica si la construcción es válida; el código modificado a continuación funciona con MSVC ++ y gcc, y el OP ha publicado más código modificado que también funciona con clang.
Este es un comportamiento indefinido, porque el lambda interno captura el parámetro
self
por referencia, peroself
sale del alcance después de lareturn
línea 7. Por lo tanto, cuando el lambda devuelto se ejecuta más tarde, está accediendo a una referencia a una variable que se ha salido del alcance.Ejecutar el programa con
valgrind
ilustra esto:En su lugar, puede cambiar el lambda externo para tomarlo por referencia en lugar de por valor, evitando así un montón de copias innecesarias y también resolviendo el problema:
Esto funciona:
fuente
self
una referencia?self
una referencia, este problema desaparece , pero Clang todavía lo rechaza por otra razónself
se haya capturado por referencia!TL; DR;
Clang es correcto.
Parece que la sección del estándar que hace que este mal formado sea [dcl.spec.auto] p9 :
Trabajo original a través de
Si miramos la propuesta Una propuesta para agregar un Combinador Y a la Biblioteca estándar , proporciona una solución de trabajo:
y dice explícitamente que tu ejemplo no es posible:
y hace referencia a una discusión en la que Richard Smith alude al error que te está dando el sonido metálico :
Barry me señaló la propuesta de seguimiento Lambdas recursivas que explica por qué esto no es posible y evita la
dcl.spec.auto#9
restricción y también muestra métodos para lograr esto hoy sin ella:fuente
self
No parece una entidad así.Parece que el sonido metálico es correcto. Considere un ejemplo simplificado:
Vamos a verlo como un compilador (un poco):
it
esLambda1
con un operador de llamada de plantilla.it(it);
desencadena la creación de instancias del operador de llamadaauto
, por lo que debemos deducirlo.Lambda1
.self(self)
self(self)
es exactamente con lo que comenzamos!Como tal, el tipo no puede deducirse.
fuente
Lambda1::operator()
es simplementeLambda2
. Luego, dentro de esa expresión lambda interna , se sabe que el tipo de retorno deself(self)
, una llamada deLambda1::operator()
, también esLambda2
. Posiblemente las reglas formales se interponen en el camino de hacer esa deducción trivial, pero la lógica presentada aquí no lo hace. La lógica aquí solo equivale a una afirmación. Si las reglas formales se interponen en el camino, entonces eso es un defecto en las reglas formales.Bueno, tu código no funciona. Pero esto hace:
Código de prueba:
Su código es UB y está mal formado, no se requiere diagnóstico. Lo cual es gracioso; pero ambos se pueden arreglar de forma independiente.
Primero, la UB:
esto es UB porque las tomas externas
self
por valor, luego las capturas internasself
por referencia, luego proceden a devolverlo después de queouter
termina de ejecutarse. Entonces segfaulting definitivamente está bien.La solución:
El código permanece está mal formado. Para ver esto podemos expandir las lambdas:
esto crea instancias
__outer_lambda__::operator()<__outer_lambda__>
:Entonces, a continuación tenemos que determinar el tipo de retorno de
__outer_lambda__::operator()
.Lo revisamos línea por línea. Primero creamos
__inner_lambda__
tipo:Ahora, mire allí: su tipo de retorno es
self(self)
, o__outer_lambda__(__outer_lambda__ const&)
. Pero estamos en el medio de tratar de deducir el tipo de retorno de__outer_lambda__::operator()(__outer_lambda__)
.No tienes permitido hacer eso.
Si bien, de hecho, el tipo de retorno de
__outer_lambda__::operator()(__outer_lambda__)
no depende realmente del tipo de retorno de__inner_lambda__::operator()(int)
, C ++ no le importa al deducir los tipos de retorno; simplemente verifica el código línea por línea.Y
self(self)
se usa antes de deducirlo. Programa mal formado.Podemos parchear esto escondiéndonos
self(self)
hasta más tarde:y ahora el código es correcto y se compila. Pero creo que esto es un poco hack; solo usa el combinador.
fuente
operator()
, en general, no se puede deducir hasta que se instancia (al ser llamado con algún argumento de algún tipo). Y, por lo tanto, una reescritura manual similar a una máquina a un código basado en plantillas funciona bien.Es bastante fácil reescribir el código en términos de las clases que un compilador generaría, o más bien debería generar, para las expresiones lambda.
Una vez hecho esto, está claro que el problema principal es solo la referencia colgante, y que un compilador que no acepta el código es algo desafiado en el departamento de lambda.
La reescritura muestra que no hay dependencias circulares.
Una versión con plantilla para reflejar la forma en que la lambda interna del código original captura un elemento de tipo plantilla:
Supongo que es esta plantilla en la maquinaria interna, que las reglas formales están diseñadas para prohibir. Si ellos prohíben la construcción original.
fuente
template< class > class Inner;
plantillaoperator()
es ... instanciada? Bueno, palabra equivocada. ¿Escrito? ... duranteOuter::operator()<Outer>
antes de deducir el tipo de retorno del operador externo. YInner<Outer>::operator()
tiene un llamado aOuter::operator()<Outer>
sí mismo. Y eso no está permitido. Ahora, la mayoría de los compiladores no notan elself(self)
porque esperan para deducir el tipo de retorno deOuter::Inner<Outer>::operator()<int>
para cuandoint
se pasa en. Sensible. Pero echa de menos la mala forma del código.Innner<T>::operator()<U>
se instancia esa plantilla de función . Después de todo, el tipo de retorno podría depender delU
aquí. No lo hace, pero en general.