¿Por qué se excluyen los parámetros de valor por defecto de NRVO?

Imagina:

S f(S a) { return a; } 

¿Por qué no está permitido alias ay el valor de retorno?

 S s = f(t); S s = t; // can't generally transform it to this :( 

La especificación no permite esta transformación si el constructor de copia de S tiene efectos secundarios. En su lugar, requiere al menos dos copias (una de t a a , una de a para el valor de retorno y otra del valor de retorno para s , y solo la última puede elide. Tenga en cuenta que escribí = t arriba para representan el hecho de una copia de t a f’s a , la única copia que aún sería obligatoria en presencia de efectos secundarios del constructor de movimiento / copia).

¿Porqué es eso?

He aquí por qué la elisión de copia no tiene sentido para los parámetros. Realmente se trata de la implementación del concepto a nivel de comstackdor.

Copiar elisión funciona básicamente construyendo el valor de retorno en el lugar. El valor no se copia; se crea directamente en su destino previsto. Es la persona que llama la que proporciona el espacio para la salida prevista, y por lo tanto, en última instancia, es la persona que llama la que proporciona la posibilidad de elisión.

Todo lo que la función necesita internamente para eludir la copia es construir la salida en el lugar provisto por la persona que llama. Si la función puede hacer esto, obtienes elisión de copia. Si la función no puede, entonces usará una o más variables temporales para almacenar los resultados intermedios, luego la copiará / moverá al lugar provisto por la persona que llama. Aún se construye in situ, pero la construcción de la salida se realiza mediante copia.

Por lo tanto, el mundo que está fuera de una función en particular no tiene que saber o preocuparse acerca de si una función realiza una elisión. Específicamente, quien llama a la función no tiene que saber cómo se implementa la función. No está haciendo nada diferente; es la función en sí misma la que decide si la elisión es posible.

La persona que llama también proporciona almacenamiento para los parámetros de valor. Cuando llamas a f(t) , es la persona que llama la que crea la copia de t y la pasa a f . De manera similar, si S es implícitamente construible desde un int , entonces f(5) construirá una S partir de la 5 y la pasará a f .

Todo esto lo hace la persona que llama . El llamado no sabe ni le importa que haya sido una variable o una temporal; simplemente se le da un lugar de memoria de stack (o registros o lo que sea).

Ahora recuerde: copiar elisión funciona porque la función que se llama construye la variable directamente en la ubicación de salida. Por lo tanto, si intenta eludir el retorno de un parámetro de valor, entonces el almacenamiento para el parámetro de valor también debe ser el almacenamiento de salida en sí . Pero recuerde: es la persona que llama la que proporciona ese almacenamiento para el parámetro y la salida. Y, por lo tanto, para eludir la copia de salida, la persona que llama debe construir el parámetro directamente en la salida .

Para hacer esto, ahora la persona que llama necesita saber que la función a la que llama borrará el valor de retorno, ya que solo puede pegar el parámetro directamente en la salida si se devuelve el parámetro. En general, esto no será posible a nivel del comstackdor, porque la persona que llama no necesariamente tiene la implementación de la función. Si la función está en línea, entonces quizás funcione. Pero de lo contrario no.

Por lo tanto, el comité de C ++ no se molestó en permitir esta posibilidad.

El razonamiento, tal como lo entiendo, para esa restricción es que la convención de llamadas podría (y en muchos casos) exigir que el argumento para la función y el objeto de retorno se encuentren en diferentes ubicaciones (memoria o registros). Considere el siguiente ejemplo modificado:

 X foo(); X bar( X a ) { return a; } int main() { X x = bar( foo() ); } 

En teoría, el conjunto completo de copias sería return statement en foo ( $tmp1 ), argumento a de bar , return statement de bar ( $tmp2 ) x en main . Los comstackdores pueden eludir dos de los cuatro objetos creando $tmp1 en la ubicación de a y $tmp2 en la ubicación de x . Cuando el comstackdor está procesando main , puede notar que el valor de retorno de foo es el argumento para bar y puede hacer que coincidan, en ese punto no puede saber (sin subrayar) que el argumento y la devolución de la bar son el mismo objeto, y tiene que cumplir con la convención de llamadas, por lo que colocará $tmp1 en la posición del argumento en la bar .

Al mismo tiempo, sabe que el propósito de $tmp2 es solo crear x , por lo que puede ubicar ambos en la misma dirección. Dentro de la bar , no hay mucho que se pueda hacer: el argumento a se ubica en lugar del primer argumento, de acuerdo con la convención de llamadas, y $tmp2 tiene que ubicarse de acuerdo con la convención de llamadas, (en el caso general en una diferente ubicación, piense que el ejemplo se puede extender a una bar que toma más argumentos, solo uno de los cuales se usa como statement de devolución.

Ahora, si el comstackdor realiza la alineación, podría detectar que la copia adicional que se requeriría si la función no estaba en línea realmente no es necesaria, y tendría una posibilidad de eludirla. Si el estándar permite que se elimine esa copia en particular, entonces el mismo código tendría diferentes comportamientos dependiendo de si la función está en línea o no.

De t a a no es razonable copiar elide. El parámetro se declara mutable, por lo que se realiza el copiado porque se espera que se modifique en la función.

De a para devolver el valor no puedo ver ninguna razón para copiar. Tal vez es algún tipo de supervisión? Los parámetros de by-value se sienten como locales dentro del cuerpo de la función … no veo diferencia allí.

David Rodríguez – las dribeas respondieron a mi pregunta ‘Cómo permitir la construcción de copia elision para las clases de C ++’ me dieron la siguiente idea. El truco es usar lambdas para retrasar la evaluación hasta dentro del cuerpo de la función:

 #include  struct S { S() {} S(const S&) { std::cout << "Copy" << std::endl; } S(S&&) { std::cout << "Move" << std::endl; } }; S f1(S a) { return a; } S f2(const S& a) { return a; } #define DELAY(x) [&]{ return x; } template  S f3(const F& a) { return a(); } int main() { S t; std::cout << "Without delay:" << std::endl; S s1 = f1(t); std::cout << "With delay:" << std::endl; S s2 = f3(DELAY(t)); std::cout << "Without delay pass by ref:" << std::endl; S s3 = f2(t); std::cout << "Without delay pass by ref (temporary) (should have 0 copies, will get 1):" << std::endl; S s4 = f2(S()); std::cout << "With delay (temporary) (no copies, best):" << std::endl; S s5 = f3(DELAY(S())); } 

Estas salidas en ideone GCC 4.5.1:

Sin retraso:
Dupdo
Dupdo
Con retraso:
Dupdo

Ahora esto es bueno, pero uno podría sugerir que la versión DELAY es como pasar por la referencia constante, como se muestra a continuación:

Sin demora pase por ref:
Dupdo

Pero si pasamos una referencia temporal por const, aún recibimos una copia:

Sin demora pase por ref (temporal) (debe tener 0 copias, obtendrá 1):
Dupdo

Donde la versión retrasada elides la copia:

Con retraso (temporal) (sin copias, mejor):

Como puede ver, esto elimina todas las copias en el caso temporal.

La versión demorada produce una copia en el caso no temporal, y no copias en el caso de un temporal. No conozco otra forma de lograr esto aparte de lambdas, pero estaría interesado si existe.

Siento que la alternativa siempre está disponible para la optimización:

 S& f(S& a) { return a; } // pass & return by reference ^^^ ^^^ 

Si f() está codificado como se menciona en su ejemplo, entonces está perfectamente bien suponer que se pretende copiar o se esperan efectos secundarios; de lo contrario, ¿por qué no elegir el pase / devolución por referencia?

Supongamos que si se aplica NRVO (como usted pregunta), entonces no hay diferencia entre S f(S) y S& f(S&) !

NRVO activa las situaciones como operator +() ( ejemplo ) porque no hay una alternativa válida.

Un aspecto de apoyo, todas las funciones siguientes tienen diferentes comportamientos para copiar:

 S& f(S& a) { return a; } // 0 copy S f(S& a) { return a; } // 1 copy S f(S a) { A a1; return (...)? a : a1; } // 2 copies 

En el tercer fragmento, si se sabe que (...) en el momento de la comstackción es false , el comstackdor genera solo 1 copia.
Esto significa que el comstackdor no realiza la optimización a propósito cuando hay una alternativa trivial disponible.

Creo que el problema es que si el constructor de copia hace algo, entonces el comstackdor debe hacer eso una cantidad de veces predecible. Si tiene una clase que incrementa un contador cada vez que se copia, por ejemplo, y hay una forma de acceder a ese contador, entonces un comstackdor que cumpla con los estándares debe hacer esa operación un número bien definido de veces (de lo contrario, ¿cómo se escribiría? pruebas unitarias?)

Ahora, probablemente sea una mala idea escribir una clase así, pero no es tarea del comstackdor averiguarlo, solo para asegurarse de que la salida sea correcta y consistente.