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

Respuestas:

129

Se permitió que la elisión de copias ocurriera en varias circunstancias. Sin embargo, incluso si estuviera permitido, el código aún tenía que poder funcionar como si la copia no se hubiera eliminado. Es decir, tenía que haber un constructor de copia y / o movimiento accesible.

Garantizada copia elisión redefine una serie de conceptos C ++, de tal manera que ciertas circunstancias en las que se podrían elididas copias / se mueve en realidad no provocan una copia / mover en absoluto . El compilador no está eliminando una copia; el estándar dice que tal copia nunca podría ocurrir.

Considere esta función:

T Func() {return T();}

Bajo las reglas de elisión de copia no garantizadas, esto creará un valor temporal, luego pasará de ese temporal al valor de retorno de la función. Esa operación de movimiento se puede elidir, pero Taú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 tcon el valor de retorno de Func. Sin embargo, Ttodavía debe tener un constructor de movimientos, aunque no se llamará.

La elisión de copia garantizada redefine el significado de una expresión de valor . Pre-C ++ 17, prvalues ​​son objetos temporales. En C ++ 17, una expresión prvalue es simplemente algo que puede materializar un temporal, pero todavía no es temporal.

Si usa un prvalue para inicializar un objeto del tipo prvalue, no se materializa ningún temporal. Cuando lo hace return T();, esto inicializa el valor de retorno de la función a través de un prvalue. Dado que esa función regresa T, no se crea ningún 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 de un objeto, tal como T()es.

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

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

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

La elisión garantizada también funciona con inicialización directa:

new T(FactoryFunction());

Si se FactoryFunctiondevuelve Tpor valor, esta expresión no copiará el valor devuelto en la memoria asignada. En su lugar, asignará memoria y utilizará la memoria asignada como memoria de valor de retorno para la llamada de función directamente.

Entonces, las funciones de fábrica que regresan por valor pueden inicializar directamente la memoria asignada al montón sin siquiera saberlo. Siempre que estos funcionen internamente sigan las reglas de elisión de copia garantizada, 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 tipos.


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, taún debe tener un constructor de copia / movimiento accesible. Sí, el compilador puede optar por optimizar la copia / movimiento. Pero el compilador aún debe verificar la existencia de un constructor de copia / movimiento accesible.

Así que nada cambia para la optimización del valor de retorno con nombre (NRVO).

Nicol Bolas
fuente
1
@BenVoigt: Poner tipos definidos por el usuario que no se pueden copiar trivialmente en los registros no es algo viable que una ABI pueda hacer, ya sea que la elisión esté disponible o no.
Nicol Bolas
1
Ahora que las reglas son públicas, puede valer la pena actualizar esto con el concepto "prvalues ​​son inicializaciones".
Johannes Schaub - litb
6
@ JohannesSchaub-litb: Es sólo "ambiguo" si sabe demasiado sobre las minucias del estándar C ++. Para el 99% de la comunidad de C ++, sabemos a qué se refiere la "eliminación de copia garantizada". El documento real que propone la función se titula incluso "Elisión de copia garantizada". Agregar "mediante categorías de valor simplificadas" simplemente hace que sea confuso y difícil de entender para los usuarios. También es un nombre inapropiado, ya que estas reglas en realidad no "simplifican" las reglas sobre categorías de valor. Le guste o no, el término "elisión de copia garantizada" se refiere a esta función y nada más.
Nicol Bolas
1
Tengo muchas ganas de poder recoger un valor y llevarlo consigo. Supongo que esto es solo un (one-shot) en std::function<T()>realidad.
Yakk - Adam Nevraumont
1
@LukasSalich: Esa es una pregunta de C ++ 11. Esta respuesta es sobre una característica de C ++ 17.
Nicol Bolas