todas - ¿Por qué pasa el valor(si se necesita una copia) recomendado en C++ 11 si una referencia constante solo cuesta una sola copia?
tipos de funciones en lenguaje c (5)
Al consumir datos, necesitará un objeto que pueda consumir. Cuando obtenga una std::string const&
tendrá que copiar el objeto independientemente de si el argumento será necesario.
Cuando el objeto se pasa por valor, el objeto se copiará si debe copiarse, es decir, cuando el objeto pasado no sea temporal. Sin embargo, si resulta ser temporal, el objeto puede construirse en su lugar, es decir, puede que se hayan eliminado todas las copias y solo pague por la construcción de un movimiento. Es decir, existe la posibilidad de que no ocurra ninguna copia.
Estoy intentando comprender la semántica de movimientos, las referencias rvalue, std::move
, etc. He estado tratando de descubrir, mediante la búsqueda de varias preguntas en este sitio, por qué se pasa una const std::string &name
+ _name(name)
menos recomendado que std::string name
+ _name(std::move(name))
si se necesita una copia.
Si entiendo correctamente, lo siguiente requiere una sola copia (a través del constructor) más un movimiento (del temporal al miembro):
Dog::Dog(std::string name) : _name(std::move(name)) {}
La forma alternativa (y pasada de moda) es pasarla por referencia y copiarla (de la referencia al miembro):
Dog::Dog(const std::string &name) : _name(name) {}
Si el primer método requiere una copia y mueve ambos, y el segundo método solo requiere una sola copia, ¿cómo se puede preferir el primer método y, en algunos casos, más rápido?
Considere llamar a las distintas opciones con un lvalue y con un valor r:
Dog::Dog(const std::string &name) : _name(name) {}
Ya sea que se llame con un lvalue o rvalue, esto requiere exactamente una copia, para inicializar
_name
desde elname
. Moverse no es una opción porque elname
esconst
.Dog::Dog(std::string &&name) : _name(std::move(name)) {}
Esto solo se puede invocar con un valor r, y se moverá.
Dog::Dog(std::string name) : _name(std::move(name)) {}
Cuando se llama con un lvalue, esto se copiará para pasar el argumento y luego se moverá para completar el miembro de datos. Cuando se le llama con un valor r, esto se moverá para pasar el argumento, y luego se moverá para completar el miembro de datos. En el caso del valor r, se puede eludir el movimiento para pasar el argumento. Por lo tanto, llamar esto con un valor l resulta en una copia y un movimiento, y llamar esto con un valor r resulta en uno o dos movimientos.
La solución óptima es definir tanto (1)
como (2)
. La solución (3)
puede tener un movimiento adicional relativo al óptimo. Pero escribir una función es más corto y más fácil de mantener que escribir dos funciones prácticamente idénticas, y se supone que los movimientos son baratos.
Cuando se realiza una llamada con un valor implícitamente convertible en cadena como const char*
, se produce la conversión implícita que implica un cálculo de longitud y una copia de los datos de cadena. Luego caemos en los casos rvalue. En este caso, usar un string_view
proporciona otra opción más:
Dog::Dog(std::string_view name) : _name(name) {}
Cuando se llama con una cadena lvalue o rvalue, esto da como resultado una copia. Cuando se llama con un
const char*
, se realiza un cómputo de longitud y una copia.
Fuera de los motivos de rendimiento, cuando una copia arroja una excepción en un constructor de valor por defecto, se lanza a la persona que llama primero y no dentro del propio constructor. Esto hace que sea más fácil codificar constructores sin excepción y no tener que preocuparse por las filtraciones de recursos o un bloque try / catch en un constructor.
struct A {
std::string a;
A( ) = default;
~A( ) = default;
A( A && ) noexcept = default;
A &operator=( A && ) noexcept = default;
A( A const &other ) : a{other.a} {
throw 1;
}
A &operator=( A const &rhs ) {
if( this != &rhs ) {
a = rhs.a;
throw 1;
}
return *this;
}
};
struct B {
A a;
B( A value ) try : a { std::move( value ) }
{ std::cout << "B constructor/n"; }
catch( ... ) {
std::cerr << "Exception in B initializer/n";
}
};
struct C {
A a;
C( A const &value ) try : a { value }
{ std::cout << "C constructor/n"; }
catch( ... ) {
std::cerr << "Exception in C initializer/n";
}
};
int main( int, char ** ) {
try {
A a;
B b{a};
} catch(...) { std::cerr << "Exception outside B2/n"; }
try {
A a;
C c{a};
} catch(...) { std::cerr << "Exception outside C/n"; }
return EXIT_SUCCESS;
}
Producirá
Exception outside B2
Exception in C initializer
Exception outside C
Hice un experimento:
#include <cstdio>
#include <utility>
struct Base {
Base() { id++; }
static int id;
};
int Base::id = 0;
struct Copyable : public Base {
Copyable() = default;
Copyable(const Copyable &c) { printf("Copyable [%d] is copied/n", id); }
};
struct Movable : public Base {
Movable() = default;
Movable(Movable &&m) { printf("Movable [%d] is moved/n", id); }
};
struct CopyableAndMovable : public Base {
CopyableAndMovable() = default;
CopyableAndMovable(const CopyableAndMovable &c) {
printf("CopyableAndMovable [%d] is copied/n", id);
}
CopyableAndMovable(CopyableAndMovable &&m) {
printf("CopyableAndMovable [%d] is moved/n", id);
}
};
struct TEST1 {
TEST1() = default;
TEST1(Copyable c) : q(std::move(c)) {}
TEST1(Movable c) : w(std::move(c)) {}
TEST1(CopyableAndMovable c) : e(std::move(c)) {}
Copyable q;
Movable w;
CopyableAndMovable e;
};
struct TEST2 {
TEST2() = default;
TEST2(Copyable const &c) : q(c) {}
// TEST2(Movable const &c) : w(c)) {}
TEST2(CopyableAndMovable const &c) : e(std::move(c)) {}
Copyable q;
Movable w;
CopyableAndMovable e;
};
int main() {
Copyable c1;
Movable c2;
CopyableAndMovable c3;
printf("1/n");
TEST1 z(c1);
printf("2/n");
TEST1 x(std::move(c2));
printf("3/n");
TEST1 y(c3);
printf("4/n");
TEST2 a(c1);
printf("5/n");
TEST2 s(c3);
printf("DONE/n");
return 0;
}
Y aqui esta el resultado:
1
Copyable [4] is copied
Copyable [5] is copied
2
Movable [8] is moved
Movable [10] is moved
3
CopyableAndMovable [12] is copied
CopyableAndMovable [15] is moved
4
Copyable [16] is copied
5
CopyableAndMovable [21] is copied
DONE
Conclusión:
template <typename T>
Dog::Dog(const T &name) : _name(name) {}
// if T is only copyable, then it will be copied once
// if T is only movable, it results in compilation error (conclusion: define separate move constructor)
// if T is both copyable and movable, it results in one copy
template <typename T>
Dog::Dog(T name) : _name(std::move(name)) {}
// if T is only copyable, then it results in 2 copies
// if T is only movable, and you called Dog(std::move(name)), it results in 2 moves
// if T is both copyable and movable, it results in one copy, then one move.
Respuesta corta primero: llamar por const y siempre costará una copia. Dependiendo de las condiciones, la llamada por valor solo puede costar un movimiento . Pero depende (por favor, eche un vistazo a los ejemplos de código a continuación para el escenario al que se refiere esta tabla):
lvalue rvalue unused lvalue unused rvalue
------------------------------------------------------
const& copy copy - -
rvalue&& - move - -
value copy, move move copy -
T&& copy move - -
overload copy move - -
Entonces mi resumen ejecutivo sería que la llamada por valor vale la pena ser considerada si
- el movimiento es barato, ya que podría haber un movimiento adicional
- el parámetro se usa incondicionalmente Llamar por valor también cuesta una copia si el parámetro no se utiliza, por ejemplo, debido a una cláusula if o sth.
Llamar por valor
Considere una función que se utiliza para copiar su argumento
class Dog {
public:
void name_it(const std::string& newName) { names.push_back(newName); }
private:
std::vector<std::string> names;
};
En caso de que se pase un valor name_it
a name_it
tendrá dos operaciones de copia en caso de un valor r. Eso es malo porque la rvalue podría moverme.
Una posible solución sería escribir una sobrecarga para los valores r:
class Dog {
public:
void name_it(const std::string& newName) { names.push_back(newName); }
void name_it(std::string&& newName) { names.push_back(std::move(newName)); }
private:
std::vector<std::string> names;
};
Eso resuelve el problema y todo está bien, a pesar de que tiene dos funciones de código dos con exactamente el mismo código.
Otra solución viable sería usar el reenvío perfecto, pero eso también tiene varias desventajas (por ejemplo, las funciones de reenvío perfectas son bastante codiciosas y hacen que una configuración y una función de sobrecarga existentes sean inútiles, normalmente necesitarán estar en un archivo de cabecera, crean varias funciones en el código objeto y algo más).
class Dog {
public:
template<typename T>
void name_it(T&& in_name) { names.push_back(std::forward<T>(in_name)); }
private:
std::vector<std::string> names;
};
Aún Otra solución sería usar llamada por valor :
class Dog {
public:
void name_it(std::string newName) { names.push_back(std::move(newName)); }
private:
std::vector<std::string> names;
};
Lo importante es, como mencionaste el std::move
. De esta forma, tendrá una función para rvalue y lvalue. Moverá los valores r pero aceptará un movimiento adicional para los valores l, lo que podría estar bien si el movimiento es barato y copia o mueve el parámetro independientemente de las condiciones.
Así que, al final, creo que es completamente incorrecto recomendar una manera sobre las demás. Depende fuertemente
#include <vector>
#include <iostream>
#include <utility>
using std::cout;
class foo{
public:
//constructor
foo() {}
foo(const foo&) { cout << "/tcopy/n" ; }
foo(foo&&) { cout << "/tmove/n" ; }
};
class VDog {
public:
VDog(foo name) : _name(std::move(name)) {}
private:
foo _name;
};
class RRDog {
public:
RRDog(foo&& name) : _name(std::move(name)) {}
private:
foo _name;
};
class CRDog {
public:
CRDog(const foo& name) : _name(name) {}
private:
foo _name;
};
class PFDog {
public:
template <typename T>
PFDog(T&& name) : _name(std::forward<T>(name)) {}
private:
foo _name;
};
//
volatile int s=0;
class Dog {
public:
void name_it_cr(const foo& in_name) { names.push_back(in_name); }
void name_it_rr(foo&& in_name) { names.push_back(std::move(in_name));}
void name_it_v(foo in_name) { names.push_back(std::move(in_name)); }
template<typename T>
void name_it_ur(T&& in_name) { names.push_back(std::forward<T>(in_name)); }
private:
std::vector<foo> names;
};
int main()
{
std::cout << "--- const& ---/n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_cr(my_foo);
std::cout << "rvalue:";
b.name_it_cr(foo());
}
std::cout << "--- rvalue&& ---/n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue: -/n";
std::cout << "rvalue:";
a.name_it_rr(foo());
}
std::cout << "--- value ---/n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_v(my_foo);
std::cout << "rvalue:";
b.name_it_v(foo());
}
std::cout << "--- T&&--/n";
{
Dog a,b;
foo my_foo;
std::cout << "lvalue:";
a.name_it_ur(my_foo);
std::cout << "rvalue:";
b.name_it_ur(foo());
}
return 0;
}
Salida:
--- const& ---
lvalue: copy
rvalue: copy
--- rvalue&& ---
lvalue: -
rvalue: move
--- value ---
lvalue: copy
move
rvalue: move
--- T&&--
lvalue: copy
rvalue: move