c++ - retornan - ¿Es la construcción de paso por valor-y-luego-movimiento un mal lenguaje?
pasaje de argumentos por valor (5)
Como tenemos semántica de movimiento en C ++, hoy en día es habitual hacer
void set_a(A a) { _a = std::move(a); }
El razonamiento es que si a
es un valor, la copia será eliminada y solo habrá un movimiento.
Pero, ¿qué pasa si a
es un valor? Parece que habrá una construcción de copia y luego una asignación de movimiento (suponiendo que A tenga un operador de asignación de movimiento adecuado). Mover las asignaciones puede ser costoso si el objeto tiene demasiadas variables miembro.
Por otro lado, si lo hacemos.
void set_a(const A& a) { _a = a; }
Habrá solo una copia asignada. ¿Podemos decir que se prefiere esta manera a la expresión paso a valor si pasamos valores l?
Pero, ¿qué pasa si
a
es un valor? Parece que habrá una construcción de copia y luego una asignación de movimiento (suponiendo que A tenga un operador de asignación de movimiento adecuado). Mover las asignaciones puede ser costoso si el objeto tiene demasiadas variables miembro.
Problema bien manchado. No me atrevería a decir que la construcción de pasar por valor y luego mover es una mala expresión, pero definitivamente tiene sus peligros potenciales.
Si su tipo de movimiento es costoso y / o el movimiento es esencialmente solo una copia, entonces el enfoque de paso por valor es subóptimo. Ejemplos de tales tipos incluirían tipos con una matriz de tamaño fijo como miembro: puede ser relativamente costoso moverlo y un movimiento es solo una copia. Ver también
- Optimización de cadenas pequeñas y operaciones de movimiento y
- "¿Quieres velocidad? Medir". (por Howard Hinnant)
en este contexto.
El enfoque de paso por valor tiene la ventaja de que solo necesita mantener una función, pero paga por esto con el rendimiento. Depende de su aplicación si esta ventaja de mantenimiento supera la pérdida de rendimiento.
El método de paso por valor de referencia de rvalue y rvalue puede llevar a problemas de mantenimiento rápidamente si tiene varios argumentos. Considera esto:
#include <vector>
using namespace std;
struct A { vector<int> v; };
struct B { vector<int> v; };
struct C {
A a;
B b;
C(const A& a, const B& b) : a(a), b(b) { }
C(const A& a, B&& b) : a(a), b(move(b)) { }
C( A&& a, const B& b) : a(move(a)), b(b) { }
C( A&& a, B&& b) : a(move(a)), b(move(b)) { }
};
Si tiene varios argumentos, tendrá un problema de permutación. En este ejemplo tan simple, probablemente no sea tan malo mantener estos 4 constructores. Sin embargo, ya en este caso simple, consideraría seriamente usar el enfoque de paso por valor con una sola función
C(A a, B b) : a(move(a)), b(move(b)) { }
En lugar de los 4 constructores anteriores.
Tan larga la historia corta, ninguno de los enfoques es sin inconvenientes. Tome sus decisiones basándose en la información de perfil real, en lugar de optimizar de forma prematura.
Los tipos caros para mover son raros en el uso moderno de C ++. Si le preocupa el costo de la mudanza, escriba ambas sobrecargas:
void set_a(const A& a) { _a = a; }
void set_a(A&& a) { _a = std::move(a); }
o un modificador de reenvío perfecto:
template <typename T>
void set_a(T&& a) { _a = std::forward<T>(a); }
eso aceptará valores de l, valores de r, y cualquier otra cosa convertible implícitamente a decltype(_a)
sin requerir copias o movimientos adicionales.
A pesar de que requiere un movimiento adicional cuando se establece desde un valor l, el idioma no es malo, ya que (a) la gran mayoría de los tipos proporciona movimientos de tiempo constante y (b) la función de copia e intercambio proporciona una seguridad excepcional y un rendimiento casi óptimo en una sola línea de código.
Me estoy respondiendo porque trataré de resumir algunas de las respuestas. ¿Cuántos movimientos / copias tenemos en cada caso?
(A) Pase por valor y mueva la construcción de asignación, pasando un parámetro X. Si X es un ...
Temporal: 1 movimiento (la copia es eluida)
Valor: 1 copia 1 movimiento
std :: move (lvalue): 2 movimientos
(B) Pasar por referencia y copiar la construcción habitual (antes de C ++ 11). Si X es un ...
Temporal: 1 copia
Valor: 1 copia
std :: move (lvalue): 1 copia
Podemos asumir que los tres tipos de parámetros son igualmente probables. Entonces, cada 3 llamadas tenemos (A) 4 movimientos y 1 copia, o (B) 3 copias. Es decir, en promedio, (A) 1.33 movimientos y 0.33 copias por llamada o (B) 1 copia por llamada.
Si llegamos a una situación en la que nuestras clases consisten principalmente de POD, los movimientos son tan caros como las copias. Entonces tendríamos 1.66 copias (o movimientos) por llamada al setter en el caso (A) y 1 copias en el caso (B).
Podemos decir que en algunas circunstancias (tipos basados en PODs), la construcción de pasar por valor y luego mover es una idea muy mala. Es un 66% más lento y depende de una característica de C ++ 11.
Por otro lado, si nuestras clases incluyen contenedores (que hacen uso de la memoria dinámica), (A) debería ser mucho más rápido (excepto si en su mayoría pasamos los valores l).
Por favor corrígeme si estoy equivocado.
Para el caso general donde se almacenará el valor , el paso por valor solo es un buen compromiso
En el caso de que sepa que solo se pasarán lvalues (algunos códigos estrechamente acoplados) no es razonable, inteligente.
Para el caso en el que se sospecha una mejora de la velocidad al proporcionar ambos, primero PIENSE DOS VECES, y si eso no ayuda, MEDIDA.
Donde no se almacenará el valor, prefiero la transferencia por referencia, ya que evita innumerables operaciones de copia innecesarias.
Finalmente, si la programación pudiera reducirse a una aplicación de reglas irreflexiva, podríamos dejarla en manos de los robots. Así que en mi humilde opinión no es una buena idea centrarse tanto en las reglas. Mejor enfocarse en cuáles son las ventajas y costos, para diferentes situaciones. Los costos incluyen no solo la velocidad, sino también, por ejemplo, el tamaño y la claridad del código. Las reglas generalmente no pueden manejar tales conflictos de intereses.
Pase por valor, luego mover es en realidad un buen idioma para objetos que sabe que son móviles.
Como mencionó, si se pasa un rvalor, se ocultará la copia o se moverá, luego dentro del constructor se moverá.
Podría sobrecargar el constructor de copia y mover el constructor explícitamente, sin embargo, se vuelve más complicado si tiene más de un parámetro.
Considere el ejemplo,
class Obj {
public:
Obj(std::vector<int> x, std::vector<int> y)
: X(std::move(x)), Y(std::move(y)) {}
private:
/* Our internal data. */
std::vector<int> X, Y;
}; // Obj
Supongamos que si desea proporcionar versiones explícitas, terminará con 4 constructores así:
class Obj {
public:
Obj(std::vector<int> &&x, std::vector<int> &&y)
: X(std::move(x)), Y(std::move(y)) {}
Obj(std::vector<int> &&x, const std::vector<int> &y)
: X(std::move(x)), Y(y) {}
Obj(const std::vector<int> &x, std::vector<int> &&y)
: X(x), Y(std::move(y)) {}
Obj(const std::vector<int> &x, const std::vector<int> &y)
: X(x), Y(y) {}
private:
/* Our internal data. */
std::vector<int> X, Y;
}; // Obj
Como puede ver, a medida que aumenta el número de parámetros, el número de constructores necesarios crece en permutaciones.
Si no tiene un tipo concreto pero tiene un constructor templado, puede usar el reenvío perfecto como:
class Obj {
public:
template <typename T, typename U>
Obj(T &&x, U &&y)
: X(std::forward<T>(x)), Y(std::forward<U>(y)) {}
private:
std::vector<int> X, Y;
}; // Obj
Referencias: