¿Es legal usar el operador de incremento en una llamada de función C++?
function standards (8)
Ha habido cierto debate en esta pregunta sobre si el siguiente código es legal C ++:
std::list<item*>::iterator i = items.begin();
while (i != items.end())
{
bool isActive = (*i)->update();
if (!isActive)
{
items.erase(i++); // *** Is this undefined behavior? ***
}
else
{
other_code_involving(*i);
++i;
}
}
El problema aquí es que erase()
invalidará el iterador en cuestión. Si eso sucede antes de que se evalúe i++
, entonces incrementarlo i
gusta es un comportamiento técnicamente indefinido, incluso si parece funcionar con un compilador particular. Un lado del debate dice que todos los argumentos de funciones se evalúan completamente antes de llamar a la función. El otro lado dice, "las únicas garantías son que i ++ ocurrirá antes de la siguiente declaración y después de que se use i ++. Ya sea que se invoque antes de borrar (i ++) o después depende el compilador".
Abrí esta pregunta para poder resolver ese debate.
++ Kristo!
El estándar de C ++ 1.9.16 tiene mucho sentido con respecto a cómo uno implementa el operador ++ (postfix) para una clase. Cuando se llama a ese operador ++ (int), se incrementa y devuelve una copia del valor original. Exactamente como dice la especificación C ++.
¡Es bueno ver cómo mejoran los estándares!
Sin embargo, recuerdo claramente el uso de compiladores de C anteriores (ANSI) en los que:
foo -> bar(i++) -> charlie(i++);
No hiciste lo que piensas! En su lugar compiló equivalente a:
foo -> bar(i) -> charlie(i); ++i; ++i;
Y este comportamiento dependía de la implementación del compilador. (Hacer porting divertido)
Es bastante fácil probar y verificar que los compiladores modernos ahora se comporten correctamente:
#define SHOW(S,X) cout << S << ": " # X " = " << (X) << endl
struct Foo
{
Foo & bar(const char * theString, int theI)
{ SHOW(theString, theI); return *this; }
};
int
main()
{
Foo f;
int i = 0;
f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i);
SHOW("END ",i);
}
Respondiendo para comentar en hilo ...
... Y aprovechando la mayoría de las respuestas de TODOS ... (¡Gracias, muchachos!)
Creo que necesitamos deletrear esto un poco mejor:
Dado:
baz(g(),h());
Entonces no sabemos si g () se invocará antes o después de h () . Es "no especificado" .
Pero sí sabemos que tanto g () como h () se invocarán antes que baz () .
Dado:
bar(i++,i++);
De nuevo, no sabemos qué i ++ se evaluará primero, y quizás ni siquiera si se incrementará una o dos veces antes de que se llame a bar () . ¡Los resultados no están definidos! (Dado i = 0 , esto podría ser bar (0,0) o bar (1,0) o bar (0,1) ¡ o algo realmente extraño!)
Dado:
foo(i++);
Ahora sabemos que se incrementará antes de invocar a foo () . Como señaló Kristo en la sección estándar de C ++ 1.9.16:
Cuando se llama a una función (esté o no en línea), cada cómputo de valor y efecto secundario asociado con cualquier expresión de argumento, o con la expresión de postfijo que designa la función llamada, se secuencia antes de la ejecución de cada expresión o declaración en el cuerpo del llamada función. [Nota: los cómputos de valor y los efectos secundarios asociados con las diferentes expresiones de argumentos no se han secuenciado. - nota final]
Aunque creo que la sección 5.2.6 lo dice mejor:
El valor de una expresión postfix ++ es el valor de su operando. [Nota: el valor obtenido es una copia del valor original - nota final] El operando debe ser un valor l modificable. El tipo del operando debe ser un tipo aritmético o un puntero a un tipo de objeto efectivo completo. El valor del objeto operando se modifica agregando 1 a él, a menos que el objeto sea de tipo bool, en cuyo caso se establece en verdadero. [Nota: este uso está en desuso, ver Anexo D. - nota final] El cálculo del valor de la expresión ++ se secuencia antes de la modificación del objeto del operando. Con respecto a una llamada a función de secuencia indeterminada, la operación de postfix ++ es una única evaluación. [Nota: Por lo tanto, una llamada a función no debe intervenir entre la conversión lvalue-a-rvalue y el efecto secundario asociado con cualquier operador postfix ++ único. - nota final] El resultado es un valor r. El tipo de resultado es la versión cv no calificada del tipo de operando. Ver también 5.7 y 5.17.
El estándar, en la sección 1.9.16, también enumera (como parte de sus ejemplos):
i = 7, i++, i++; // i becomes 9 (valid)
f(i = -1, i = -1); // the behavior is undefined
Y podemos demostrarlo trivialmente con:
#define SHOW(X) cout << # X " = " << (X) << endl
int i = 0; /* Yes, it''s global! */
void foo(int theI) { SHOW(theI); SHOW(i); }
int main() { foo(i++); }
Entonces, sí, i se incrementa antes de invocar a foo () .
Todo esto tiene mucho sentido desde la perspectiva de:
class Foo
{
public:
Foo operator++(int) {...} /* Postfix variant */
}
int main() { Foo f; delta( f++ ); }
Aquí Foo :: operator ++ (int) debe invocarse antes de delta () . Y la operación de incremento debe completarse durante esa invocación.
En mi ejemplo (quizás demasiado complejo):
f . bar("A",i) . bar("B",i++) . bar("C",i) . bar("D",i);
f.bar ("A", i) debe ejecutarse para obtener el objeto utilizado para object.bar ("B", i ++) , y así sucesivamente para "C" y "D" .
Entonces sabemos que i ++ incrementa i antes de llamar a la barra ("B", i ++) (aunque barra ("B", ...) se invoca con el valor anterior de i ), y por lo tanto i se incrementa antes de la barra ( "C", i) y barra ("D", i) .
Volviendo al comentario de j_random_hacker :
j_random_hacker escribe:
+1, pero tuve que leer el estándar cuidadosamente para convencerme de que esto estaba bien. Estoy en lo cierto al pensar que, si bar () era en cambio una función global que devuelve decir int, f era un int, y esas invocaciones estaban conectadas por decir "^" en lugar de ".", Entonces cualquiera de A, C y D podría reportar "0"?
Esta pregunta es mucho más complicada de lo que piensas ...
Reescribiendo tu pregunta como código ...
int bar(const char * theString, int theI) { SHOW(...); return i; }
bar("A",i) ^ bar("B",i++) ^ bar("C",i) ^ bar("D",i);
Ahora solo tenemos UNA expresión De acuerdo con el estándar (sección 1.9, página 8, pdf página 20):
Nota: los operadores pueden reagruparse de acuerdo con las reglas matemáticas habituales solo cuando los operadores son realmente asociativos o conmutativos. (7) Por ejemplo, en el siguiente fragmento: a = a + 32760 + b + 5; la declaración de expresión se comporta exactamente igual que: a = (((a + 32760) + b) +5); debido a la asociatividad y precedencia de estos operadores. Por lo tanto, el resultado de la suma (a + 32760) se agrega a continuación a b, y ese resultado luego se agrega a 5, lo que da como resultado el valor asignado a a. En una máquina en la que los desbordamientos producen una excepción y en la que el rango de valores representables por un int es [-32768, + 32767], la implementación no puede reescribir esta expresión como a = ((a + b) +32765); dado que si los valores para a y b fueran, respectivamente, -32754 y -15, la suma a + b produciría una excepción mientras que la expresión original no lo haría; ni tampoco se puede reescribir la expresión como a = ((a + 32765) + b); o a = (a + (b + 32765)); ya que los valores para a y b podrían haber sido, respectivamente, 4 y -8 o -17 y 12. Sin embargo, en una máquina en la que los desbordamientos no producen una excepción y en los que los desbordamientos son reversibles, la declaración de expresión anterior puede ser reescrito por la implementación en cualquiera de las formas anteriores porque ocurrirá el mismo resultado. - nota final]
Entonces, podríamos pensar que, debido a la precedencia, nuestra expresión sería la misma que:
(
(
( bar("A",i) ^ bar("B",i++)
)
^ bar("C",i)
)
^ bar("D",i)
);
Pero, como (a ^ b) ^ c == a ^ (b ^ c) sin posibles situaciones de desbordamiento, podría reescribirse en cualquier orden ...
Pero, como se invoca a bar () y podría hipotéticamente implicar efectos secundarios, esta expresión no se puede reescribir en cualquier orden. Las reglas de precedencia aún se aplican.
Que muy bien determina el orden de evaluación de la barra () ''s .
Ahora, cuando ocurre eso i + = 1 ? Bueno, todavía tiene que ocurrir antes de invocar la barra ("B", ...) . (Aunque barra ("B", ....) se invoca con el valor anterior).
Entonces ocurre determinísticamente antes de la barra (C) y la barra (D) , y después de la barra (A) .
Respuesta: NO . Siempre tendremos "A = 0, B = 0, C = 1, D = 1", si el compilador cumple con los estándares.
Pero considere otro problema:
i = 0;
int & j = i;
R = i ^ i++ ^ j;
¿Cuál es el valor de R?
Si i + = 1 ocurrió antes de j , tendríamos 0 ^ 0 ^ 1 = 1. Pero si i + = 1 ocurriera después de toda la expresión, tendríamos 0 ^ 0 ^ 0 = 0.
De hecho, R es cero. El i + = 1 no ocurre hasta después de que la expresión ha sido evaluada.
Lo que considero es por qué:
i = 7, i ++, i ++; // me hago 9 (válido)
Es legal ... Tiene tres expresiones:
- i = 7
- i ++
- i ++
Y en cada caso, el valor de i se cambia al final de cada expresión. (Antes de evaluar cualquier expresión posterior)
PD: considera:
int foo(int theI) { SHOW(theI); SHOW(i); return theI; }
i = 0;
int & j = i;
R = i ^ i++ ^ foo(j);
En este caso, i + = 1 debe evaluarse antes de foo (j) . theI es 1. Y R es 0 ^ 0 ^ 1 = 1.
El gurú de la semana # 55 de Sutter (y la pieza correspondiente en "C ++ más excepcional") discute este caso exacto como un ejemplo.
Según él, es un código perfectamente válido, y de hecho un caso en el que se intenta transformar la declaración en dos líneas:
items.erase(i); i++;
no produce código semánticamente equivalente a la declaración original.
El estándar dice que el efecto secundario ocurre antes de la llamada, por lo que el código es el mismo que:
std::list<item*>::iterator i_before = i;
i = i_before + 1;
items.erase(i_before);
en lugar de ser:
std::list<item*>::iterator i_before = i;
items.erase(i);
i = i_before + 1;
Por lo tanto, es seguro en este caso, porque list.erase () específicamente no invalida ningún iterador distinto del que se borró.
Dicho esto, es un mal estilo: la función de borrado de todos los contenedores devuelve el siguiente iterador específicamente, por lo que no tiene que preocuparse por la invalidación de los iteradores debido a la reasignación, por lo que el código idiomático:
i = items.erase(i);
será seguro para las listas, y también será seguro para los vectores, deques y cualquier otro contenedor de secuencia si desea cambiar su almacenamiento.
Tampoco obtendrías el código original para compilar sin advertencias, tendrías que escribir
(void)items.erase(i++);
para evitar una advertencia sobre un retorno no utilizado, lo que sería una gran pista de que estás haciendo algo extraño.
Está perfectamente bien. El valor pasado sería el valor de "i" antes del incremento.
Para construir sobre la respuesta de Kristo ,
foo(i++, i++);
produce un comportamiento indefinido porque el orden en que se evalúan los argumentos de la función no está definido (y en el caso más general porque si lee una variable dos veces en una expresión donde también la escribe, el resultado no está definido). Usted no sabe qué argumento se incrementará primero.
int i = 1;
foo(i++, i++);
podría dar como resultado una llamada de función de
foo(2, 1);
o
foo(1, 2);
o incluso
foo(1, 1);
Ejecute lo siguiente para ver qué sucede en su plataforma:
#include <iostream>
using namespace std;
void foo(int a, int b)
{
cout << "a: " << a << endl;
cout << "b: " << b << endl;
}
int main()
{
int i = 1;
foo(i++, i++);
}
En mi máquina consigo
$ ./a.out
a: 2
b: 1
cada vez, pero este código no es portátil , por lo que esperaría ver diferentes resultados con diferentes compiladores.
Para construir sobre la respuesta de Bill the Lizard:
int i = 1;
foo(i++, i++);
también podría dar como resultado una llamada a función de
foo(1, 1);
(lo que significa que los valores reales se evalúan en paralelo y luego se aplican las postes).
- MarkusQ
Para construir sobre la respuesta de MarkusQ:;)
O más bien, el comentario de Bill:
( Editar: Aw, el comentario se ha ido otra vez ... bueno)
Se les permite ser evaluados en paralelo. Si esto sucede o no en la práctica es técnicamente irrelevante.
Sin embargo, no necesita el paralelismo de hilos para que esto ocurra, solo evalúe el primer paso de ambos (tome el valor de i) antes del segundo (incremente i). Perfectamente legal, y algunos compiladores pueden considerarlo más eficiente que evaluar por completo un i ++ antes de comenzar el segundo.
De hecho, espero que sea una optimización común. Míralo desde un punto de vista de programación de instrucciones. Usted tiene lo siguiente que necesita evaluar:
- Toma el valor de i para el argumento correcto
- Incrementa i en el argumento correcto
- Toma el valor de i para el argumento de la izquierda
- Incrementar i en el argumento de la izquierda
Pero en realidad no hay dependencia entre el argumento de la izquierda y el de la derecha. La evaluación de argumentos ocurre en un orden no especificado, y no es necesario hacerlo secuencialmente tampoco (razón por la cual new () en los argumentos de función suele ser una pérdida de memoria, incluso cuando está envuelto en un puntero inteligente). También está indefinido lo que sucede cuando se modifica la misma variable dos veces en la misma expresión. Sin embargo, tenemos una dependencia entre 1 y 2, y entre 3 y 4. Entonces, ¿por qué el compilador esperaría que 2 se completaran antes de calcular 3? Esto introduce latencia agregada, y tomará más tiempo de lo necesario antes de que 4 esté disponible. Suponiendo que hay una latencia de 1 ciclo entre cada uno, tomará 3 ciclos desde que 1 esté completo hasta que el resultado de 4 esté listo y podamos llamar a la función.
Pero si los reordenamos y evaluamos en el orden 1, 3, 2, 4, podemos hacerlo en 2 ciclos. 1 y 3 se pueden iniciar en el mismo ciclo (o incluso combinados en una instrucción, ya que es la misma expresión), y en el siguiente, se pueden evaluar 2 y 4. Todas las CPU modernas pueden ejecutar 3-4 instrucciones por ciclo, y un buen compilador debería intentar explotar eso.
Quoth el estándar C ++ 1.9.16:
Cuando se llama a una función (esté o no en línea), cada cómputo de valor y efecto secundario asociado con cualquier expresión de argumento, o con la expresión de postfijo que designa la función llamada, se secuencia antes de la ejecución de cada expresión o declaración en el cuerpo del llamada función. (Nota: los cómputos de valor y los efectos secundarios asociados con las diferentes expresiones de argumento no se han secuenciado).
Entonces me parece que este código:
foo(i++);
es perfectamente legal Incrementará i
y luego llamará a foo
con el valor anterior de i
. Sin embargo, este código:
foo(i++, i++);
produce un comportamiento indefinido porque el párrafo 1.9.16 también dice:
Si un efecto secundario en un objeto escalar no se está secuenciando en relación con otro efecto secundario en el mismo objeto escalar o con un cálculo de valor que utiliza el valor del mismo objeto escalar, el comportamiento no está definido.