lenguaje - sintaxis de c++
Expresión de C++ postfix indefinida vs comportamiento no especificado (5)
El comportamiento no está especificado, porque en la llamada a la función (myLambda(n))(n)
la expresión de posfijo (que le di un par de paréntesis redundantes adicionales) no está relacionada con la expresión de argumento n
(la más a la derecha). Sin embargo, no hay un comportamiento indefinido, porque la modificación de n
tiene lugar dentro de la función llamada myLambda(n)
. La única relación de secuenciación definida es que tanto la evaluación de myLambda(n)
como la n
final se secuencian obviamente antes de la llamada real de la lambda. Para la pregunta final, si la lambda decide ignorar completamente su argumento, entonces el comportamiento ya no se especifica: aunque no se especifica qué valor se pasa como parámetro, no hay diferencia observable de ninguna manera.
Disculpas de antemano, sé que el tema general de la orden de evaluación ya ha tenido muchas SO preguntas. Sin embargo, después de mirarlos, quiero aclarar algunos puntos específicos que no creo que equivalgan a una duplicación de nada. Supongamos que tengo el siguiente código:
#include <iostream>
auto myLambda(int& n)
{
++n;
return [](int param) { std::cout << "param: " << param << std::endl; };
}
int main()
{
int n{0};
myLambda(n)(n);
return 0;
}
El programa anterior genera "n: 0" cuando lo compilo. Aquí tenemos un ordenamiento no especificado en juego: podría tener la misma facilidad con la que "n: 1" tuvo un orden de evaluación diferente.
Mis preguntas son:
¿Cuál es exactamente la relación de secuencia en juego durante la invocación final de la función anterior (es decir, la invocación de la expresión lambda), entre la expresión postfix
myLambda(0)
, su argumenton
y la función subsiguiente que se llama a sí misma?¿Es lo anterior un ejemplo de comportamiento indefinido o no especificado , y por qué exactamente (con referencia al estándar)?
Si cambié el código lambda a
[](int param) { std::cout << "hello" << std::endl }
(es decir, hice el resultado independiente de su parámetro y por lo tanto cualquier decisión de orden de evaluación, haciendo que el comportamiento sea determinista) ¿la respuesta a 2) de arriba sería igual?
EDITAR : He cambiado el nombre del parámetro lambda de ''n'' a ''param'' porque eso parecía estar causando confusión.
El orden en que se evalúan las subexpresiones no está especificado y puede variar aparte de los operadores &&, || ,? y ",".
El compilador conoce tanto los prototipos de función myLambda como también el retorno lambda (extraído del tipo de retorno). Porque en mi primera oración el compilador es libre, qué expresión evalúa primero. Es por eso que nunca debe llamar a las funciones en la expresión que tienen efectos secundarios adicionales.
Irónicamente (dado que el ejemplo usa las características de C ++ 11 y otras respuestas se han distraído por eso) la lógica que hace que esta muestra tenga un comportamiento no especificado se remonta a C ++ 98, Sección 5, párrafo 4
Excepto donde se indique, el orden de evaluación de los operandos de operadores individuales y las subexpresiones de expresiones individuales, y el orden en el que se producen los efectos secundarios, no se especifica. Entre el punto de secuencia anterior y el siguiente, un objeto escalar tendrá su valor almacenado modificado a lo sumo una vez por la evaluación de una expresión. Además, solo se debe acceder al valor anterior para determinar el valor que se almacenará. Los requisitos de este párrafo se cumplirán para cada ordenamiento permitido de las subexpresiones de una expresión completa; De lo contrario el comportamiento es indefinido.
Esencialmente, la misma cláusula existe en todos los estándares de C ++, aunque, como se señaló en el comentario de Marc van Leeuwen, los estándares recientes de C ++ ya no usan el concepto de puntos de secuencia. El efecto neto es el mismo: dentro de una declaración, el orden o la evaluación de los operandos de los operadores y las subexpresiones de expresiones individuales no se especifica.
El comportamiento no especificado se produce porque una expresión n
se evalúa dos veces en la declaración
myLambda(n)(n);
Una evaluación de la expresión n
(para obtener una referencia) está asociada con la primera (n)
y otra evaluación de una expresión n
(para obtener un valor) está asociada con la segunda (n)
. El orden de evaluación de esas dos expresiones (aunque sean, ópticamente, ambas n
) no está especificado.
Existen cláusulas similares en TODOS los estándares de C ++ y tienen el mismo resultado: comportamiento no especificado en la declaración myLambda(n)(n)
, independientemente de cómo se myLambda()
implementado myLambda()
Por ejemplo, myLambda()
podría implementarse en C ++ 98 (y en todos los estándares de C ++ posteriores, incluido C ++ 11 y posterior), como este
class functor
{
functor() {};
int operator()(int n) { std::cout << "n: " << n << std::endl; };
};
functor myLambda(int &n)
{
++n;
return functor();
}
int main()
{
int n = 0;
myLambda(n)(n);
return 0;
}
ya que el código en la pregunta es solo una técnica (C ++ 11) (o taquigrafía) para lograr el mismo efecto que este.
Lo anterior responde a las preguntas 1 y 2 de OP. El comportamiento no especificado ocurre en main()
, no tiene relación con la forma en que se implementa myLambda()
.
Para responder a la tercera pregunta del OP, el comportamiento aún no se especifica si la lambda (o el operator()
del functor operator()
) en mi ejemplo) se modifica para no acceder al valor de su argumento. La única diferencia es que el programa en su conjunto no produce resultados visibles que pueden variar entre los compiladores.
La n
en la definición de la lambda es un argumento formal de la función que define la lambda. No tiene conexión con el argumento de myLambda
que también se llama n
. El comportamiento aquí está dictado completamente por la forma en que se llaman estas dos funciones. En myLambda(n)(n)
el orden de evaluación de los dos argumentos de función no está especificado. La lambda se puede llamar con un argumento de 0 o 1, dependiendo del compilador.
Si la lambda se hubiera definido con [=n]()...
se comportaría de manera diferente.
No logré encontrar una referencia adecuada al estándar, pero veo que tiene un comportamiento similar al orden de evaluación de argumentos solicitado here y el orden de evaluación de los argumentos de función no está definido por la norma:
5.2.2 Llamada de función
8 [ Nota: Las evaluaciones de la expresión de posfijo y de las expresiones de argumento no están sujetas entre sí. Todos los efectos secundarios de las evaluaciones de expresiones de argumento se secuencian antes de que se ingrese a la función (ver 1.9). "Nota final "
Así que aquí está cómo va dentro de las llamadas en diferentes compiladores:
#include <iostream>
#include <functional>
struct Int
{
Int() { std::cout << "Int(): " << v << std::endl; }
Int(const Int& o) { v = o.v; std::cout << "Int(const Int&): " << v << std::endl; }
Int(int o) { v = o; std::cout << "Int(int): " << v << std::endl; }
~Int() { std::cout << "~Int(): " << v << std::endl; }
Int& operator=(const Int& o) { v = o.v; std::cout << "operator= " << v << std::endl; return *this; }
int v;
};
namespace std
{
template<>
Int&& forward<Int>(Int& a) noexcept
{
std::cout << "Int&: " << a.v << std::endl;
return static_cast<Int&&>(a);
}
template<>
Int&& forward<Int>(Int&& a) noexcept
{
std::cout << "Int&&: " << a.v << std::endl;
return static_cast<Int&&>(a);
}
}
std::function<void(Int)> myLambda(Int& n)
{
std::cout << "++n: " << n.v << std::endl;
++n.v;
return [&](Int m) {
std::cout << "n: " << m.v << std::endl;
};
}
int main()
{
Int n(0);
myLambda(n)(n);
return 0;
}
GCC g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
y g++ -std=c++14 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
Int (int): 0
Int (const Int &): 0
++ n: 0
Int &: 0
Int &: 0
Int (const Int &): 0
n: 0
~ Int (): 0
~ Int (): 0
~ Int (): 1
Así que crea una variable y la copia para pasarla a lamba devuelto.
Clang clang++ -std=c++14 main.cpp && ./a.out
Int (int): 0
++ n: 0
Int (const. Int &): 1
Int y: 1
Int y: 1
Int (const. Int &): 1
n: 1
~ Int (): 1
~ Int (): 1
~ Int (): 1
Aquí crea una función de evaluación de variables y luego pasa la copia del lamba.
Y el orden de evaluación es:
struct A
{
A(int) { std::cout << "1" << std::endl; }
~A() { std::cout << "-1" << std::endl; }
};
struct B
{
B(double) { std::cout << "2" << std::endl; }
~B() { std::cout << "-2" << std::endl; }
};
void f(A, B) { }
int main()
{
f(4, 5.);
}
MSVC y GCC:
2
1
-1
-2
Sonido metálico:
1
2
-2
-1
Como en Clang, el orden es hacia adelante y el argumento a lambda se pasa después de evaluar el argumento de la función.