c++ optimization macros c++11

Cómo permitir copiar la construcción de elision para las clases de C++(no solo las estructuras POD C)



optimization macros (4)

... pero la construcción de b2 requiere un movimiento.

No, no lo hace. Al compilador se le permite evitar el movimiento; Si eso sucede es específico de la implementación, dependiendo de varios factores. También se permite mover, pero no puede copiar (el movimiento debe usarse en lugar de copiar en esta situación).

Es cierto que no se le garantiza que la mudanza será rechazada. Si se le debe garantizar que no se producirá ningún movimiento, entonces use la macro o investigue las opciones de su implementación para controlar este comportamiento, en particular la función de alineación.

Considere el siguiente código:

#include <iostream> #include <type_traits> struct A { A() {} A(const A&) { std::cout << "Copy" << std::endl; } A(A&&) { std::cout << "Move" << std::endl; } }; template <class T> struct B { T x; }; #define MAKE_B(x) B<decltype(x)>{ x } template <class T> B<T> make_b(T&& x) { return B<T> { std::forward<T>(x) }; } int main() { std::cout << "Macro make b" << std::endl; auto b1 = MAKE_B( A() ); std::cout << "Non-macro make b" << std::endl; auto b2 = make_b( A() ); }

Esto da como resultado lo siguiente:

Macro hacer b
No-macro hacer b
Movimiento

Tenga en cuenta que b1 se construye sin movimiento, pero la construcción de b2 requiere un movimiento.

También necesito escribir deducción, ya que A en el uso en la vida real puede ser un tipo complejo que es difícil de escribir explícitamente. También necesito poder anidar llamadas (es decir, make_c(make_b(A())) ).

¿Es posible tal función?

Más pensamientos:

N3290 Final C ++ 0x borrador página 284:

Esta elision de operaciones de copia / movimiento, llamada copia elision, se permite en las siguientes circunstancias:

cuando un objeto de clase temporal que no se ha vinculado a una referencia (12.2) se copiaría / movería a un objeto de clase con el mismo tipo no cualificado de CV, la operación de copiar / mover se puede omitir construyendo el objeto temporal directamente en el destino de la copia / movimiento omitido

Desafortunadamente, esto parece que no podemos evitar copias (y movimientos) de los parámetros de la función para que funcionen los resultados (incluidos los constructores), ya que esos temporales están vinculados a una referencia (cuando se pasan por referencia) o ya no son temporales (cuando se pasan por valor). Parece que la única forma de evitar todas las copias al crear un objeto compuesto es crearlo como un agregado. Sin embargo, los agregados tienen ciertas restricciones, como exigir que todos los miembros sean públicos y no constructores definidos por el usuario.

No creo que tenga sentido para C ++ permitir optimizaciones para la construcción agregada de CODs de POD, pero no permitir las mismas optimizaciones para la construcción de clase C ++ no POD.

¿Hay alguna manera de permitir copiar / mover elision para construcciones no agregadas?

Mi respuesta:

Esta construcción permite que las copias sean eluidas para los tipos que no son POD. Tengo esta idea de la respuesta de David Rodríguez a continuación. Requiere C ++ 11 lambdas. En este ejemplo a continuación, he cambiado make_b para tomar dos argumentos para hacer las cosas menos triviales. No hay llamadas a ningún constructor de movimiento o copia.

#include <iostream> #include <type_traits> struct A { A() {} A(const A&) { std::cout << "Copy" << std::endl; } A(A&&) { std::cout << "Move" << std::endl; } }; template <class T> class B { public: template <class LAMBDA1, class LAMBDA2> B(const LAMBDA1& f1, const LAMBDA2& f2) : x1(f1()), x2(f2()) { std::cout << "I''m a non-trivial, therefore not a POD./n" << "I also have private data members, so definitely not a POD!/n"; } private: T x1; T x2; }; #define DELAY(x) [&]{ return x; } #define MAKE_B(x1, x2) make_b(DELAY(x1), DELAY(x2)) template <class LAMBDA1, class LAMBDA2> auto make_b(const LAMBDA1& f1, const LAMBDA2& f2) -> B<decltype(f1())> { return B<decltype(f1())>( f1, f2 ); } int main() { auto b1 = MAKE_B( A(), A() ); }

Si alguien sabe cómo lograr esto de manera más clara, estaría muy interesado en verlo.

Discusión previa:

Esto se deriva de las respuestas a las siguientes preguntas:

¿Se puede optimizar la creación de objetos compuestos a partir de temporarios?
Evitando la necesidad de #define con plantillas de expresión
Eliminando copias innecesarias al construir objetos compuestos


Como Anthony ya ha mencionado, el estándar prohíbe la elección de copias desde el argumento de una función hasta la devolución de la misma función. La razón que impulsa esa decisión es que la elección de copia (y la elección de movimiento) es una optimización mediante la cual dos objetos en el programa se combinan en la misma ubicación de memoria, es decir, la copia se elimina al tener ambos objetos uno. La cita estándar (parcial) se encuentra a continuación, seguida de un conjunto de circunstancias bajo las cuales se permite la copia de la copia, que no incluyen ese caso en particular.

Entonces, ¿qué hace que ese caso particular sea diferente? Básicamente, la diferencia es que el hecho de que exista una llamada de función entre los objetos originales y los copiados, y la llamada de función implica que hay restricciones adicionales a considerar, en particular la convención de llamada.

Dada una función T foo( T ) y un usuario que llama T x = foo( T(param) ); En el caso general, con una compilación separada, el compilador creará un objeto $tmp1 en la ubicación en la que la convención de llamada requiere que el primer argumento sea. Luego llamará a la función e inicializará x desde la declaración de retorno. Esta es la primera oportunidad para la elección de copia: colocando cuidadosamente x en la ubicación donde se encuentra el temporal devuelto, x y el objeto devuelto de foo convierten en un solo objeto, y esa copia se elimina. Hasta aquí todo bien. El problema es que la convención de llamada en general no tendrá el objeto devuelto y el parámetro en la misma ubicación, y debido a eso, $tmp1 y x no pueden ser una única ubicación en la memoria.

Sin ver la definición de la función, el compilador no puede saber que el único propósito del argumento de la función es servir como declaración de retorno y, como tal, no puede ignorar esa copia adicional. Se puede argumentar que si la función está en inline entonces el compilador tendrá la información adicional que falta para comprender que el temporizador utilizado para llamar a la función, el valor devuelto x son un solo objeto. El problema es que esa copia en particular solo se puede eliminar si el código está realmente en línea (no solo si está marcado como en inline sino en realidad ) Si se requiere una llamada de función, entonces la copia no se puede eliminar. Si el estándar permitía que esa copia se eliminara cuando el código estaba en línea, implicaría que el comportamiento de un programa diferiría debido al compilador y no al código de usuario: la palabra clave en inline no obliga a ingresar, solo significa que existen múltiples definiciones. De la misma función no representan una violación de la ODR.

Tenga en cuenta que si la variable se creó dentro de la función (en comparación con la que se pasó a ella) como en: T foo() { T tmp; ...; return tmp; } T x = foo(); T foo() { T tmp; ...; return tmp; } T x = foo(); entonces ambas copias pueden eliminarse: no hay restricciones a partir de dónde se debe crear tmp (no es un parámetro de entrada o salida para la función, por lo que el compilador puede reubicarlo en cualquier lugar, incluida la ubicación del tipo devuelto, y en el lado de la llamada, x puede, como en el ejemplo anterior, ubicarse cuidadosamente en la ubicación de esa misma declaración de devolución, lo que básicamente significa que tmp , la declaración de devolución y x pueden ser un solo objeto.

A partir de su problema particular, si recurre a una macro, el código está en línea, no hay restricciones en los objetos y la copia puede ser eliminada. Pero si agrega una función, no puede ignorar la copia del argumento a la declaración de retorno. Así que solo evítalo. En lugar de usar una plantilla que mueva el objeto, cree una plantilla que construya un objeto:

template <typename T, typename... Args> T create( Args... x ) { return T( x... ); }

Y esa copia puede ser eliminada por el compilador.

Tenga en cuenta que no he tratado con la construcción de movimientos, ya que parece preocupado por el costo de incluso la construcción de movimientos, aunque creo que está ladrando al árbol equivocado. Dado un caso de uso real motivador, estoy bastante seguro de que las personas aquí presentarán un par de ideas eficientes.

12.8 / 31

Cuando se cumplen ciertos criterios, se permite que una implementación omita la construcción de copiar / mover de un objeto de clase, incluso si el constructor y / o destructor de copia / movimiento del objeto tiene efectos secundarios. En tales casos, la implementación trata el origen y el destino de la operación de copia / movimiento omitida simplemente como dos formas diferentes de referirse al mismo objeto, y la destrucción de ese objeto ocurre en el último momento en que los dos objetos hubieran sido Destruido sin la optimización.


Esto no es un gran problema. Todo lo que necesita es cambiar la estructura del código ligeramente.

En lugar de:

B<A> create(A &&a) { ... } int main() { auto b = create(A()); }

Siempre puedes hacer:

int main() { A a; B<A> b(a); ... }

Si el constructor de B es así, entonces no tomará ninguna copia:

template<class T> class B { B(T &t) :t(t) { } T &t; };

El estuche compuesto también funcionará:

struct C { A a; B b; }; void init(C &c) { c.a = 10; c.b = 20; } int main() { C c; init(c); }

Y ni siquiera necesita las funciones de c ++ 0x para hacer esto.


No puede optimizar la copia / movimiento del objeto A desde el parámetro de make_b al miembro del objeto B creado.

Sin embargo, este es el punto central de la semántica de movimiento: al proporcionar una operación de movimiento liviano para A , puede evitar una copia potencialmente costosa. por ejemplo, si A era realmente std::vector<int> , la copia del contenido del vector se puede evitar mediante el uso del constructor de movimientos, y en su lugar solo se transferirán los punteros de mantenimiento.