c++ - pueden - Mover constructores y matrices estáticas
estructura de datos estaticas (4)
He estado explorando las posibilidades de Move Constructors en C ++, y me preguntaba cuáles son algunas formas de aprovechar esta característica en un ejemplo como el siguiente. Considere este código:
template<unsigned int N>
class Foo {
public:
Foo() {
for (int i = 0; i < N; ++i) _nums[i] = 0;
}
Foo(const Foo<N>& other) {
for (int i = 0; i < N; ++i) _nums[i] = other._nums[i];
}
Foo(Foo<N>&& other) {
// ??? How can we take advantage of move constructors here?
}
// ... other methods and members
virtual ~Foo() { /* no action required */ }
private:
int _nums[N];
};
Foo<5> bar() {
Foo<5> result;
// Do stuff with ''result''
return result;
}
int main() {
Foo<5> foo(bar());
// ...
return 0;
}
En este ejemplo anterior, si rastreamos el programa (con MSVC ++ 2011), vemos que Foo<N>::Foo(Foo<N>&&)
se llama al construir foo
, que es el comportamiento deseado. Sin embargo, si no tuviéramos Foo<N>::Foo(Foo<N>&&)
, se llamaría a Foo<N>::Foo(const Foo<N>&)
, lo que haría una operación de copia redundante .
Mi pregunta es, como se indica en el código, con este ejemplo específico que utiliza una matriz simple asignada estáticamente, ¿hay alguna forma de utilizar el constructor de movimientos para evitar esta copia redundante?
En este caso no es útil porque int
no tiene constructores de movimiento.
Sin embargo, podría ser útil si fueran cadenas, por ejemplo:
template<unsigned int N>
class Foo {
public:
// [snip]
Foo(Foo<N>&& other) {
// move each element from other._nums to _nums
std::move(std::begin(other._nums), std::end(other._nums), &_nums[0]);
}
// [snip]
private:
std::string _nums[N];
};
Ahora evitas copiar cadenas donde hará un movimiento. No estoy seguro si un compilador de C ++ 11 conforme generará un código equivalente si omite todos los constructores de copia / movimiento completamente, lo siento.
(En otras palabras, no estoy seguro de que std::move
esté especialmente definido para hacer un movimiento de elementos para matrices).
En primer lugar, hay un tipo de consejo general que dice que no debe escribir ningún constructor de copia / movimiento, operador de asignación o destructor si puede ayudarlo, y más bien debe componer su clase de componentes de alta calidad que a su vez proporcionan estos, permitiendo que las funciones generadas por defecto hagan lo correcto. (La implicación inversa es que si tiene que escribir cualquiera de esos, probablemente tenga que escribirlos todos).
Entonces, la pregunta se reduce a "¿qué clase de componente de responsabilidad única puede aprovechar la semántica de movimientos?" La respuesta general es: todo lo que gestiona un recurso . El punto es que el constructor / asignador de movimientos simplemente reasentará el recurso al nuevo objeto e invalidará el anterior, evitando así la nueva asignación (presumiblemente costosa o imposible) y la copia profunda del recurso.
El ejemplo principal es todo lo que gestiona la memoria dinámica, donde la operación de movimiento simplemente copia el puntero y pone el puntero del objeto antiguo a cero (por lo que el destructor del objeto antiguo no hace nada). Aquí hay un ejemplo ingenuo:
class MySpace
{
void * addr;
std::size_t len;
public:
explicit MySpace(std::size_t n) : addr(::operator new(n)), len(n) { }
~MySpace() { ::operator delete(addr); }
MySpace(const MySpace & rhs) : addr(::operator new(rhs.len)), len(rhs.len)
{ /* copy memory */ }
MySpace(MySpace && rhs) : addr(rhs.addr), len(rhs.len)
{ rhs.len = 0; rhs.addr = 0; }
// ditto for assignment
};
La clave es que cualquier constructor de copiar / mover hará una copia completa de las variables miembro; es solo cuando esas variables son en sí mismas manijas o punteros a recursos que puede evitar copiar el recurso, debido al acuerdo de que un objeto movido ya no se considera válido y que usted es libre de robarlo. Si no hay nada que robar, entonces no hay beneficio en el movimiento.
Para la plantilla de clase que escribiste, no hay ninguna ventaja que tomar en un constructor de movimiento.
Habría una ventaja si la matriz de miembros se asignara dinámicamente. Pero con una matriz simple como miembro, no hay nada que optimizar, solo puede copiar los valores. No hay manera de moverlos .
Por lo general, el movimiento semántico se implementa cuando su clase administra recursos . Como en su caso, la clase no gestiona los recursos, el movimiento semántico sería más parecido a la copia semántica, ya que no hay nada que mover .
Para comprender mejor cuándo se requiere movimiento semántico, considere hacer que _nums
un puntero, en lugar de una matriz:
template<unsigned int N>
class Foo {
public:
Foo()
{
_nums = new int[N](); //allocate and zeo-initialized
}
Foo(const Foo<N>& other)
{
_nums = new int[N];
for (int i = 0; i < N; ++i) _nums[i] = other._nums[i];
}
Foo(Foo<N>&& other)
{
_nums = other._nums; //move the resource
other._nums=0; //make it null
}
Foo<N> operator=(const Foo<N> & other); //implement it!
virtual ~Foo() { delete [] _nums; }
private:
int *_nums;
};