guidelines - c++ best practices
Orden de evaluación de la declaración de asignación en C++ (4)
Del estándar C ++ 11 (énfasis mío):
5.17 Operadores de asignación y asignación compuesta
1 El operador de asignación (=) y los operadores de asignación compuesta se agrupan de derecha a izquierda. Todos requieren un valor l modificable como su operando izquierdo y devuelven un valor l que se refiere al operando izquierdo. El resultado en todos los casos es un campo de bits si el operando izquierdo es un campo de bits. En todos los casos, la asignación se secuencia después del cálculo del valor de los operandos derecho e izquierdo , y antes del cálculo del valor de la expresión de asignación.
El idioma no especifica si el operando izquierdo se evalúa primero o si el operando derecho se evalúa primero. Un compilador es libre de elegir evaluar cualquier operando primero. Dado que el resultado final de su código depende del orden de evaluación de los operandos, diría que es un comportamiento no especificado en lugar de un comportamiento indefinido.
1.3.25 comportamiento no especificado
comportamiento, para una construcción de programa bien formada y datos correctos, que depende de la implementación
map<int, int> mp;
printf("%d ", mp.size());
mp[10]=mp.size();
printf("%d/n", mp[10]);
Este código produce una respuesta que no es muy intuitiva:
0 1
Entiendo por qué sucede: el lado izquierdo de la asignación devuelve la referencia al valor subyacente de
mp[10]
y al mismo tiempo crea el valor mencionado anteriormente, y solo entonces se evalúa el lado derecho, utilizando el
size()
recién calculado
size()
del mapa.
¿Se indica este comportamiento en algún lugar del estándar C ++? ¿O el orden de evaluación no está definido?
El resultado se obtuvo usando g ++ 5.2.1.
Echemos un vistazo a lo que se descompone su código:
mp.operator[](10).operator=(mp.size());
que prácticamente cuenta la historia de que en la primera parte se crea una entrada a 10 y en la segunda parte el tamaño del contenedor se asigna a la referencia entera en la posición de 10.
Pero ahora entras en el orden del problema de evaluación que no está especificado. Aquí hay un example mucho más simple.
Cuándo debería llamarse
map::size()
, antes o después de
map::operator(int const &);
?
Nadie lo sabe realmente.
Estoy seguro de que el estándar no especifica para una expresión
x = y;
qué orden
x
o
y
se evalúa en el estándar C ++ (esta es la razón por la que no puede hacer
*p++ = *p++
por ejemplo, porque
p++
no se realiza en un orden definido).
En otras palabras, para garantizar el orden
x = y;
en un orden definido, debe dividirlo en dos puntos de secuencia.
T tmp = y;
x = tmp;
(Por supuesto, en este caso particular, se podría suponer que el compilador prefiere hacer el
operator[]
antes que
size()
porque luego puede almacenar el valor directamente en el resultado del
operator[]
lugar de mantenerlo en un lugar temporal, para almacenar más tarde después de que el
operator[]
haya sido evaluado, pero estoy bastante seguro de que el compilador NO NECESITA hacerlo en ese orden)
Sí, esto está cubierto por el estándar y es un comportamiento no especificado. Este caso particular está cubierto en una reciente propuesta de estándares de C ++: N4228 : Refinando el orden de evaluación de expresiones para Idiomatic C ++ que busca refinar el orden de las reglas de evaluación para hacerlo bien especificado para ciertos casos.
Describe este problema de la siguiente manera:
El orden de evaluación de expresiones es un tema de discusión recurrente en la comunidad de C ++. En pocas palabras, dada una expresión como f (a, b, c) , el estándar deja sin especificar el orden en el que se evalúan las subexpresiones f, a, b, c . Si dos de estas subexpresiones modifican el mismo objeto sin intervenir en puntos de secuencia, el comportamiento del programa no está definido. Por ejemplo, la expresión f (i ++, i) donde i es una variable entera conduce a un comportamiento indefinido, al igual que v [i] = i ++ . Incluso cuando el comportamiento no está indefinido, el resultado de evaluar una expresión puede ser una incógnita. Considere el siguiente fragmento de programa:
#include <map> int main() { std::map<int, int> m; m[0] = m.size(); // #1 }
¿Cómo debería ser el objeto del mapa m después de la evaluación de la declaración marcada como # 1? {{0, 0}} o {{0, 1}}?
Sabemos que, a menos que se especifique que las evaluaciones de las subexpresiones no están secuenciadas, esto es del
borrador de C ++ 11 estándar
sección
1.9
Ejecución del programa
que dice:
Excepto donde se indique, las evaluaciones de operandos de operadores individuales y de subexpresiones de expresiones individuales no tienen secuencia [...]
y toda la sección
5.17
Asignación y operadores de asignación compuesta [expr.ass] dice es:
[...] En todos los casos, la asignación se secuencia después del cálculo del valor de los operandos derecho e izquierdo, y antes del cálculo del valor de la expresión de asignación. [...]
Por lo tanto, esta sección no define el orden de evaluación, pero sabemos que este no es un comportamiento indefinido ya que tanto el
operator []
como el
size()
son llamadas a funciones y la sección
1.9
nos dice (
énfasis mío
):
[...] Cuando se llama a una función (ya sea que la función esté en línea o no), cada cálculo 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 de la función llamada. [Nota: los cálculos de valores y los efectos secundarios asociados con diferentes expresiones de argumento no están secuenciados. —Nota final] Cada evaluación en la función de llamada (incluidas otras llamadas de función) que no está secuenciada específicamente antes o después de la ejecución del cuerpo de la función llamada se secuencia indeterminadamente con respecto a la ejecución de la función llamada .9 [. ..]
Tenga en cuenta que cubro el segundo ejemplo interesante de la propuesta
N4228
en la pregunta
¿Este código del "Lenguaje de programación C ++", 4a edición, sección 36.3.6, tiene un comportamiento bien definido?
.
Actualizar
Parece que una versión revisada de
N4228
fue
aceptada por el Grupo de trabajo de Evolution en la última reunión del WG21,
pero el documento (
P0145R0
) aún no está disponible.
Por lo tanto, esto posiblemente ya no se pueda especificar en C ++ 17.
Actualización 2
La revisión 3 de p0145 especificó esto y actualizó [expr.ass]p1 :
El operador de asignación (=) y los operadores de asignación compuesta se agrupan de derecha a izquierda. Todos requieren un valor l modificable como su operando izquierdo; su resultado es un valor l que se refiere al operando izquierdo. El resultado en todos los casos es un campo de bits si el operando izquierdo es un campo de bits. En todos los casos, la asignación se secuencia después del cálculo del valor de los operandos derecho e izquierdo, y antes del cálculo del valor de la expresión de asignación. El operando derecho se secuencia antes que el operando izquierdo. ...