c++ c++17 copy-elision

c++ - ¿Cómo funciona la copia garantizada de elisión?



c++17 copy-elision (1)

En la reunión de estándares de Oulu ISO C ++ 2016, el comité de estándares votó en C ++ 17 una propuesta llamada Elisión de copia garantizada a través de categorías de valores simplificadas .

¿Cómo funciona exactamente la elisión de copia garantizada? ¿Cubre algunos casos donde la elisión de copia ya estaba permitida, o son necesarios cambios de código para garantizar la elisión de copia?


Se permitió que se produjera una elisión de copia bajo varias circunstancias. Sin embargo, incluso si estaba permitido, el código todavía tenía que poder funcionar como si la copia no se hubiera eliminado. A saber, tenía que haber una copia accesible y / o un constructor de movimiento.

La elisión de copia garantizada redefine una serie de conceptos de C ++, de modo que ciertas circunstancias en las que se podrían eludir copias / movimientos en realidad no provocan una copia / movimiento en absoluto . El compilador no está eludiendo una copia; la norma dice que tal copia nunca podría suceder.

Considere esta función:

T Func() {return T();}

Según las reglas de elisión de copia no garantizadas, esto creará un valor temporal y luego pasará de ese valor temporal al valor de retorno de la función. Esa operación de movimiento puede eludirse, pero T aún debe tener un constructor de movimiento accesible, incluso si nunca se usa.

Similar:

T t = Func();

Esta es la inicialización de copia de t . Esto copiará initialize t con el valor de retorno de Func . Sin embargo, T todavía tiene que tener un constructor de movimiento, aunque no se llamará.

Elisión de copia garantizada redefine el significado de una expresión de valor . Antes de C ++ 17, los valores son objetos temporales. En C ++ 17, una expresión prvalue es simplemente algo que puede materializar un temporal, pero aún no es temporal.

Si usa un prvalue para inicializar un objeto del tipo prvalue, entonces no se materializa temporalmente. Cuando return T(); , esto inicializa el valor de retorno de la función a través de un prvalue. Como esa función devuelve T , no se crea ninguna temporal; La inicialización del prvalue simplemente inicia directamente el valor de retorno.

Lo que hay que entender es que, dado que el valor de retorno es un prvalue, todavía no es un objeto . Es simplemente un inicializador para un objeto, al igual que T() es.

Cuando haces T t = Func(); , el valor del valor de retorno inicializa directamente el objeto t ; no hay una etapa de "crear una temporal y copiar / mover". Dado que el valor de retorno de Func() es un valor equivalente equivalente a T() , t se inicializa directamente por T() , exactamente como si hubiera hecho T t = T() .

Si se usa un prvalue de cualquier otra forma, el prvalue materializará un objeto temporal, que se usará en esa expresión (o se descartará si no hay expresión). Entonces, si const T &rt = Func(); , el prvalue se materializaría de forma temporal (usando T() como inicializador), cuya referencia se almacenaría en rt , junto con el material habitual de extensión de vida útil temporal.

Una cosa que garantiza que elision le permite hacer es devolver objetos inmóviles. Por ejemplo, lock_guard no se puede copiar ni mover, por lo que no puede tener una función que lo devuelva por valor. Pero con copia de elisión garantizada, puede hacerlo.

Elisión garantizada también funciona con inicialización directa:

new T(FactoryFunction());

Si FactoryFunction devuelve T por valor, esta expresión no copiará el valor devuelto en la memoria asignada. En su lugar, asignará memoria y usará la memoria asignada como memoria de valor de retorno para la llamada de función directamente.

Por lo tanto, las funciones de fábrica que retornan por valor pueden inicializar directamente la memoria asignada en el montón sin siquiera saberlo. Siempre y cuando estas funciones sigan internamente las reglas de copia garantizada de elisión, por supuesto. Tienen que devolver un valor de tipo T

Por supuesto, esto también funciona:

new auto(FactoryFunction());

En caso de que no le guste escribir nombres de tipo.

Es importante reconocer que las garantías anteriores solo funcionan para prvalues. Es decir, no obtiene ninguna garantía al devolver una variable con nombre :

T Func() { T t = ...; ... return t; }

En este caso, t aún debe tener un constructor accesible de copiar / mover. Sí, el compilador puede elegir optimizar la copia / mover. Pero el compilador aún debe verificar la existencia de un constructor accesible de copiar / mover.

Entonces, nada cambia para la optimización del valor de retorno con nombre (NRVO).