2012-02-25 14 views
18

Es señalado en [C++ 11: 12.8/31]:¿Por qué no se permite RVO al devolver un parámetro?

está permitido Esta elisión de las operaciones de copiar/mover, llamada copia elisión, [...]:

- en una vuelta declaración en una función con un tipo de retorno de clase, cuando la expresión es el nombre de un objeto automático no volátil (que no sea una función o parámetro catch-clause) con el mismo tipo cv no calificado que el tipo de retorno de función, el la operación copiar/mover puede omitirse al construir el objeto automático directamente en el valor de retorno de la función

Esto implica

#include <iostream> 

using namespace std; 

struct X 
{ 
    X() { } 
    X(const X& other) { cout << "X(const X& other)" << endl; } 
}; 

X no_rvo(X x) { 
    cout << "no_rvo" << endl; 
    return x; 
} 

int main() { 
    X x_orig; 
    X x_copy = no_rvo(x_orig); 

    return 0; 
} 

imprimirá

X(const X& other) 
no_rvo 
X(const X& other) 

Por qué se requiere el segundo constructor de copia? ¿No puede un compilador simplemente extender la vida útil de x?

+0

En realidad, hay tres objetos que (pueden) obtener copia construida en ese ejemplo (el argumento para 'no_rvo', el valor de retorno de' no_rvo' y 'x_copy'). La construcción de 'x_copy' puede ser eliminada (construyendo el valor de retorno de' no_rvo' directamente en 'x_copy'). – Mankarse

+0

@Mankarse: Creo que eso es exactamente lo que OP está preguntando: ¿por qué no se elimina la construcción de 'x_copy'? – jweyrich

+0

@jweyrich: No, la pregunta es por qué la construcción del valor de retorno no se elimina. Lo que quiero decir es que no es correcto decir que el código _ imprimirá 'X (const X y otro) no_rvo X (const X y otro)', porque el código también podría imprimir 'X (const X y otro) no_rvo X (const X y otros) X (const X y otro) 'si la construcción de' x_copy' no se elimina. – Mankarse

Respuesta

11

Imagínese no_rvo se define en un archivo diferente que main de manera que al compilar main el compilador sólo verá la declaración

X no_rvo(X x); 

y tendrá ni idea de si el objeto de tipo X devuelto tiene cualquier relación a la discusión. De lo que se sabe en ese momento, la implementación de no_rvo podría también ser

X no_rvo(X x) { X other; return other; } 

Así por ejemplo, cuando se compila la línea

X const& x = no_rvo(X()); 

hará lo siguiente, al optimizar al máximo.

  • Generar la X temporal que se pasa a no_rvo como argumento
  • no_rvo llamada, y se unen a su valor de retorno x
  • destrucción del objeto temporal pasó a no_rvo.

Ahora, si el valor de retorno de no_rvo fuera el mismo objeto que el objeto que se le pasó, la destrucción del objeto temporal significaría la destrucción del objeto devuelto. Pero eso sería incorrecto porque el objeto devuelto está vinculado a una referencia, por lo tanto, extiende su duración más allá de esa declaración. Sin embargo, simplemente no destruir el argumento tampoco es una solución porque sería incorrecto si la definición de no_rvo es la implementación alternativa que he mostrado anteriormente. Entonces, si la función puede reutilizar un argumento como valor de retorno, pueden surgir situaciones en las que el compilador no pudo determinar el comportamiento correcto.

Tenga en cuenta que con las implementaciones comunes, el compilador no podría optimizar eso de todos modos, por lo tanto, no es una pérdida tan grande que no está formalmente permitido. También tenga en cuenta que el compilador es permitido optimizar la copia de todos modos si puede demostrar que esto no conduce a un cambio en el comportamiento observable (la llamada regla de si-si).

+0

Así que básicamente sería incompatible con la elisión (cuando se toman juntas) de construcciones de copia de argumentos y compilación separada (porque si se realiza la elisión del constructor de copia de un argumento debe ser realizada por el llamador (porque en tiempo de compilación (separado) callee _cannot_ saber si su argumento se está construyendo a partir de un temporal o no)). – Mankarse

+1

¡Gracias por la explicación! ¿Funcionaría una convención de llamadas en la cual la _función_ toma la responsabilidad de destruir sus argumentos de pasar por valor? Entonces a la persona que llama ya no le importa cómo se define no_rvo. –

4

La implementación habitual de RVO es que el código de llamada pasa la dirección de un fragmento de memoria donde la función debe construir su objeto de resultado.

Cuando el resultado de la función es directamente una variable automática que no es un argumento formal, esa variable local simplemente puede colocarse en el fragmento de memoria provisto por el llamador, y la declaración de retorno no copia en absoluto.

Para un argumento pasado por valor, el código de máquina que llama tiene que copiar-inicializar su argumento real en la ubicación del argumento formal ’ antes de saltar a la función. Para que la función coloque su resultado allí, primero tendría que destruir el objeto de argumento formal, que tiene algunos casos especiales complicados (por ejemplo, cuando esa construcción se refiere directa o indirectamente al objeto de argumento formal). Por lo tanto, en lugar de identificar la ubicación del resultado con la ubicación del argumento formal, una optimización aquí lógicamente tiene que usar un trozo de memoria proporcionado proporcionado por separado para el resultado de la función.

Sin embargo, un resultado de función que no se pasa en un registro es normalmente proporcionado por la persona que llama. Es decir, de lo que se podría hablar razonablemente como RVO, una especie de RVO disminuido, para el caso de una expresión return que denota un argumento formal, es lo que sucedería de todos modos. Y no encaja con el texto “ al construir el objeto automático directamente en el valor de retorno de la función ”.

Resumiendo, el flujo de datos que requiere que la persona que llama pase en un valor, significa que es necesariamente la persona que llama la que inicializa el almacenamiento de un argumento formal, y no la función. Por lo tanto, no se puede evitar la copia de un argumento formal en general (ese término comadreja cubre los casos especiales en los que el compilador puede hacer cosas muy especiales, en particular para el código de máquina en línea). Sin embargo, es la función que inicializa cualquier otro objeto automático local ’ s almacenamiento, y luego es ’ s no hay problema para hacer RVO.

+4

Si bien es cierto que la implementación común no sería compatible con esa optimización de todos modos, esto por sí solo no explica por qué el estándar lo prohíbe. Después de todo, se trata de una optimización y, por lo tanto, no * obligatoria *. – celtschk

+0

@celtschk no es solo la implementación común, sino * todas las implementaciones existentes. También tenga en cuenta que esta optimización no se puede realizar dentro de la función, sino que debería ser realizada por la persona que llama con el apoyo de las convenciones de llamada. Tenga en cuenta que es el que llama el único que sabe que el argumento es un * rvalue * y que el espacio se puede reutilizar.También tenga en cuenta que la persona que llama y el destinatario de la llamada tendrían que acordar en cada llamada si el argumento sería * semánticamente * destruido durante la llamada a la función o si se destruirá más tarde. –

+0

... El problema es que permitir esa optimización no es dar libertad al compilador, sino imponer un conjunto completo de requisitos para gestionar la posibilidad de que la optimización se haya llevado a cabo sin causar un comportamiento indefinido. Incluso si fuera el compilador quien eligiera ese veneno, la posibilidad de definir una interfaz de módulo se discutió en el proceso de estandarización y se dejó para una versión posterior del estándar. Si se permitió esta optimización y un solo compilador decidió hacerlo, en C++ con módulos, todos los compiladores tendrían que morder la bala. –

Cuestiones relacionadas