vectores referencia por pasar parametros parametro funciones con como arreglo array c++ c++11 language-lawyer move-semantics initialization-list

referencia - pasar array como parametro c++



¿Es realmente necesario std:: move en la lista de inicialización del constructor para miembros pesados que se pasa por valor? (4)

Mi pregunta: ¿es esta std :: move realmente necesaria? Mi punto es que el compilador ve que este p_name no se usa en el cuerpo del constructor, así que, tal vez, ¿hay alguna regla para usar la semántica de movimiento por defecto?

En general, cuando desea convertir un valor de l en un valor de r, entonces sí, necesita un std::move() . Vea también ¿Los compiladores de C ++ 11 convierten las variables locales en valores cuando pueden hacerlo durante la optimización del código?

void President::setDefaultName() { std::string local = "SO"; name = local; // std::move OR not std::move? }

Para mí, esta variable local está expirando, por lo que la semántica de movimiento podría aplicarse ... Y esto es similar a los argumentos pasados ​​por valor ...

Aquí, me gustaría que el optimizador elimine el ALTOGETHER local superfluo; desafortunadamente, no es el caso en la práctica. Las optimizaciones del compilador se complican cuando la memoria del montón entra en juego , vea BoostCon 2013 Keynote: Chandler Carruth: Optimización de las estructuras emergentes de C ++ . Una de mis conclusiones de la charla de Chandler es que los optimizadores simplemente tienden a darse por vencidos cuando se trata de acumular memoria.

Vea el código de abajo para un ejemplo decepcionante. No uso std::string en este ejemplo porque es una clase altamente optimizada con código de ensamblaje en línea, que a menudo produce código generado contraintuitivo. Para agregar daño al insulto, std::string es, en términos generales, un puntero compartido de referencia contabilizado en gcc 4.7.2 al menos ( optimización de copia en escritura , ahora prohibida por el estándar 2011 para std::string ). Así que el código de ejemplo sin std::string :

#include <algorithm> #include <cstdio> int main() { char literal[] = { "string literal" }; int len = sizeof literal; char* buffer = new char[len]; std::copy(literal, literal+len, buffer); std::printf("%s/n", buffer); delete[] buffer; }

Claramente, de acuerdo con la regla "como si", el código generado podría optimizarse para esto:

int main() { std::printf("string literal/n"); }

Lo he probado con GCC 4.9.0 y Clang 3.5 con las optimizaciones de tiempo de enlace habilitadas (LTO), y ninguna de ellas pudo optimizar el código a este nivel. Miré el código de ensamblaje generado: Ambos asignaron la memoria en el montón e hicieron la copia. Bueno, sí, eso es decepcionante.

La memoria asignada de la pila es diferente aunque:

#include <algorithm> #include <cstdio> int main() { char literal[] = { "string literal" }; const int len = sizeof literal; char buffer[len]; std::copy(literal, literal+len, buffer); std::printf("%s/n", buffer); }

He comprobado el código de ensamblaje: aquí, el compilador puede reducir el código básicamente a std::printf("string literal/n"); .

Por lo tanto, mis expectativas de que el local superfluo en su código de ejemplo podría eliminarse no están completamente respaldadas: como muestra mi último ejemplo con la matriz asignada a la pila, se puede hacer.

Imagine cientos de proyectos KLOC escritos en C ++ 03 - ¿deberíamos agregar en todas partes este std::move ?
[...]
Pero no estoy seguro: ¿pasar por valor no es una referencia universal?

"¿Quieres velocidad? Medida". (por Howard Hinnant )

Puede encontrarse fácilmente en una situación en la que realiza sus optimizaciones solo para descubrir que sus optimizaciones hicieron que el código fuera más lento. :( Mi consejo es el mismo que el de Howard Hinnant: Medida.

std::string getName() { std::string local = "Hello SO!"; return local; // std::move(local) is not needed nor probably correct }

Sí, pero tenemos reglas para este caso especial: se denomina optimización de valor de retorno (NRVO).

Recientemente leí un ejemplo de cppreference.../vector/emplace_back :

struct President { std::string name; std::string country; int year; President(std::string p_name, std::string p_country, int p_year) : name(std::move(p_name)), country(std::move(p_country)), year(p_year) { std::cout << "I am being constructed./n"; }

Mi pregunta: ¿es esta std::move realmente necesaria? Mi punto es que este p_name no se usa en el cuerpo del constructor, así que, tal vez, ¿hay alguna regla en el lenguaje para usar la semántica de movimiento por defecto?

Sería realmente molesto agregar std :: move en la lista de inicialización a cada miembro pesado (como std::string , std::vector ). Imagine cientos de proyectos KLOC escritos en C ++ 03 - ¿deberíamos agregar en todas partes este std::move ?

Esta pregunta: la respuesta de move-constructor-and-initialization-list dice:

Como regla de oro, siempre que tome algo por referencia de valor, debe usarlo dentro de std :: move, y cada vez que tome algo por referencia universal (es decir, tipo de plantilla deducido con &&), debe usarlo dentro de std :: adelante

Pero no estoy seguro: ¿pasar por valor no es una referencia universal?

[ACTUALIZAR]

Para hacer mi pregunta más clara. ¿Se pueden tratar los argumentos del constructor como XValue? Me refiero a valores caducados?

En este ejemplo AFAIK no usamos std::move :

std::string getName() { std::string local = "Hello SO!"; return local; // std::move(local) is not needed nor probably correct }

Entonces, ¿sería necesario aquí?

void President::setDefaultName() { std::string local = "SO"; name = local; // std::move OR not std::move? }

Para mí, esta variable local está expirando, por lo que la semántica de movimiento podría aplicarse ... Y esto es similar a los argumentos pasados ​​por valor ...


Mi pregunta: ¿es esta std :: move realmente necesaria? Mi punto es que este p_name no se usa en el cuerpo del constructor, así que, tal vez, ¿hay alguna regla en el lenguaje para usar la semántica de movimiento por defecto?

Por supuesto que es necesario. p_name es un lvalue, por lo tanto std::move es necesario para convertirlo en un rvalue y seleccionar el constructor de movimiento.

Eso no es solo lo que dice el idioma, y ​​si el tipo es así:

struct Foo { Foo() { cout << "ctor"; } Foo(const Foo &) { cout << "copy ctor"; } Foo(Foo &&) { cout << "move ctor"; } };

Los mandatos de idioma que copy ctor deben imprimirse si omite el movimiento. No hay opciones aquí. El compilador no puede hacer esto de manera diferente.

Sí, copia elision todavía se aplica. Pero no en su caso (lista de inicialización), vea los comentarios.

¿O su pregunta implica por qué estamos usando ese patrón?

La respuesta es que proporciona un patrón seguro cuando queremos almacenar una copia del argumento pasado, mientras nos beneficiamos de los movimientos y evitamos una explosión combinatoria de los argumentos.

Considere esta clase que contiene dos cadenas (es decir, dos objetos "pesados" para copiar).

struct Foo { Foo(string s1, string s2) : m_s1{s1}, m_s2{s2} {} private: string m_s1, m_s2; };

Así que vamos a ver qué pasa en varios escenarios.

Toma 1

string s1, s2; Foo f{s1, s2}; // 2 copies for passing by value + 2 copies in the ctor

Argh, esto es malo. 4 copias sucediendo aquí, cuando solo 2 son realmente necesarias. En C ++ 03 inmediatamente convertiríamos los argumentos Foo () en const-refs.

Tomar 2

Foo(const string &s1, const string &s2) : m_s1{s1}, m_s2{s2} {}

Ahora tenemos

Foo f{s1, s2}; // 2 copies in the ctor

¡Eso está mucho mejor!

Pero ¿qué pasa con los movimientos? Por ejemplo, desde los temporales:

string function(); Foo f{function(), function()}; // 2 moves + still 2 copies in the ctor

O cuando se mueven explícitamente valores en el ctor:

Foo f{std::move(s1), std::move(s2)}; // 2 moves + still 2 copies in the ctor

Eso no es tan bueno. Podríamos haber usado el ctor de movimiento de la string para inicializar directamente los miembros de Foo .

Toma 3

Entonces, podríamos introducir algunas sobrecargas para el constructor de Foo:

Foo(const string &s1, const string &s2) : m_s1{s1}, m_s2{s2} {} Foo(string &&s1, const string &s2) : m_s1{s1}, m_s2{s2} {} Foo(const string &s1, string &s2) : m_s1{s1}, m_s2{s2} {} Foo(string &&s1, string &&s2) : m_s1{s1}, m_s2{s2} {}

Pues bien, ahora tenemos

Foo f{function(), function()}; // 2 moves Foo f2{s1, function()}; // 1 copy + 1 move

Bueno. Pero diablos, tenemos una explosión combinatoria : todos y cada uno de los argumentos ahora deben aparecer en sus variantes const-ref + rvalue. ¿Y si conseguimos 4 cuerdas? ¿Vamos a escribir 16 ctores?

Toma 4 (la buena)

Vamos a echar un vistazo a

Foo(string s1, string s2) : m_s1{std::move(s1)}, m_s2{std::move(s2)} {}

Con esta versión:

Foo foo{s1, s2}; // 2 copies + 2 moves Foo foo2{function(), function()}; // 2 moves in the arguments + 2 moves in the ctor Foo foo3{std::move(s1), s2}; // 1 copy, 1 move, 2 moves

Dado que los movimientos son extremadamente baratos, este patrón permite beneficiarse completamente de ellos y evitar la explosión combinatoria. De hecho, podemos bajar todo el camino .

Y ni siquiera arañé la superficie de seguridad excepcional.

Como parte de una discusión más general, ahora consideremos el siguiente fragmento de código, donde todas las clases involucradas hacen una copia de s por pase por valor:

{ // some code ... std::string s = "123"; AClass obj {s}; OtherClass obj2 {s}; Anotherclass obj3 {s}; // s won''t be touched any more from here on }

Si te recibí correctamente, realmente te gustaría que el compilador en realidad se alejara en su último uso:

{ // some code ... std::string s = "123"; AClass obj {s}; OtherClass obj2 {s}; Anotherclass obj3 {std::move(s)}; // bye bye s // s won''t be touched any more from here on. // hence nobody will notice s is effectively in a "dead" state! }

Te dije por qué el compilador no puede hacer eso, pero entiendo tu punto. Tendría sentido desde cierto punto de vista: no tiene sentido hacer que s viva más tiempo que su último uso. Alimento para el pensamiento para C ++ 2x, supongo.


Hice un poco más de investigación y consultar otros foros en la red.

Desafortunadamente, parece que este std::move es necesario no solo porque el estándar de C ++ lo dice, sino que de lo contrario sería peligroso:

((crédito a Kalle Olavi Niemitalo de comp.std.c ++ - su respuesta aquí ))

#include <memory> #include <mutex> std::mutex m; int i; void f1(std::shared_ptr<std::lock_guard<std::mutex> > p); void f2() { auto p = std::make_shared<std::lock_guard<std::mutex> >(m); ++i; f1(p); ++i; }

Si f1 (p) cambia automáticamente a f1 (std :: move (p)), entonces el mutex se desbloquearía antes del segundo ++ i; declaración.

El siguiente ejemplo parece más realista:

#include <cstdio> #include <string> void f1(std::string s) {} int main() { std::string s("hello"); const char *p = s.c_str(); f1(s); std::puts(p); }

Si f1 (s) cambia automáticamente a f1 (std :: move (s)), entonces el puntero p ya no será válido después de que f1 regrese.


La regla actual, enmendada por DR1579 , es que la transformación xvalue ocurre cuando un parámetro o local NRVOable, o una expresión-id que se refiere a una variable o parámetro local, es el argumento de una declaración de return .

Esto funciona porque, claramente, después de la declaración de return , la variable no se puede usar de nuevo.

Excepto que no es el caso

struct S { std::string s; S(std::string &&s) : s(std::move(s)) { throw std::runtime_error("oops"); } }; S foo() { std::string local = "Hello SO!"; try { return local; } catch(std::exception &) { assert(local.empty()); throw; } }

Entonces, incluso para una declaración de return , en realidad no se garantiza que una variable local o un parámetro que aparezca en esa declaración sea el último uso de esa variable.

No está totalmente fuera de la cuestión que el estándar podría cambiarse para especificar que el uso "último" de una variable local está sujeto a la transformación de xvalue; El problema es definir cuál es el "último" uso. Y otro problema es que esto tiene efectos no locales dentro de una función; agregar, por ejemplo, una declaración de depuración más abajo podría significar que ya no se produce una transformación xvalue en la que confías. Incluso una regla de ocurrencia única no funcionaría, ya que una sola declaración puede ejecutarse varias veces.

¿Quizás le interese enviar una propuesta para discusión en la lista de correo de las propuestas estándar?