¿Cómo aplicar la semántica de movimiento cuando crece un vector?

93

Tengo varios std::vectorobjetos de cierta clase A. La clase no es trivial y tiene constructores de copia y constructores de movimiento definidos.

std::vector<A>  myvec;

Si relleno el vector con Aobjetos (usando por ejemplo myvec.push_back(a)), el vector crecerá de tamaño, usando el constructor de copia A( const A&)para instanciar nuevas copias de los elementos en el vector.

¿Puedo hacer cumplir de alguna manera que el constructor de movimiento de la clase se Aesté usando en su lugar?

Bertwim van Beest
fuente
5
Puede hacerlo mediante el uso de una implementación de vector sensible al movimiento.
K-Ballo
2
¿Puede ser un poco más específico sobre cómo lograr esto?
Bertwim van Beest
1
Simplemente usa una implementación de vector sensible al movimiento. Parece que la implementación de su biblioteca estándar (¿cuál es por cierto?) No es consciente de los movimientos. Puede probar con contenedores de Boost con reconocimiento de movimiento.
K-Ballo
1
Bueno, yo uso gcc 4.5.1, que es consciente de movimientos.
Bertwim van Beest
En mi código funcionó para hacer privado el constructor de copia, aunque el constructor de movimiento no tenía el explícito "noexcept".
Arne

Respuestas:

129

Debe informar a C ++ (específicamente std::vector) que su constructor de movimiento y su destructor no arroja, usando noexcept. Luego, se llamará al constructor de movimiento cuando el vector crezca.

Así es como declarar e implementar un constructor de movimiento que es respetado por std::vector:

A(A && rhs) noexcept { 
  std::cout << "i am the move constr" <<std::endl;
  ... some code doing the move ...  
  m_value=std::move(rhs.m_value) ; // etc...
}

Si el constructor no lo es noexcept, no std::vectorpuede usarlo, ya que entonces no puede asegurar las garantías de excepción exigidas por el estándar.

Para obtener más información sobre lo que se dice en el estándar, lea C ++ Move semántica y excepciones

Gracias a Bo, quien insinuó que puede tener que ver con excepciones. También considere los consejos de Kerrek SB y utilícelo emplace_backcuando sea posible. Se puede ser más rápido (pero a menudo no lo es), que puede ser más clara y compacta, pero también hay algunas trampas (especialmente con los constructores no explícitas).

Edite , a menudo lo predeterminado es lo que desea: mueva todo lo que se pueda mover, copie el resto. Para pedirlo explícitamente, escriba

A(A && rhs) = default;

Al hacer eso, obtendrá noexcepto cuando sea posible: ¿El constructor Move predeterminado está definido como noexcept?

Tenga en cuenta que las versiones anteriores de Visual Studio 2015 y anteriores no lo admitían, aunque admite la semántica de movimiento.

Johan Lundberg
fuente
Como curiosidad, ¿cómo no la impl "sabe" si la value_type's movimiento ctor es noexcept? ¿Quizás el lenguaje restringe el conjunto de candidatos a la llamada de función cuando el alcance de la llamada también es una noexceptfunción?
Lightness Races in Orbit
1
@LightnessRacesinOrbit Supongo que solo está haciendo algo como en.cppreference.com/w/cpp/types/is_move_constructible . Solo puede haber un constructor de movimiento, por lo que debe estar claramente definido por la declaración.
Johan Lundberg
@LightnessRacesinOrbit, desde entonces he aprendido que no hay una forma (estándar / útil) de saber realmente si hay un noexceptconstructor de movimientos. is_nothrow_move_constructibleserá verdadero si hay un nothrowconstructor de copia. No conozco ningún caso real de nothrowconstructores de copias costosos, por lo que no está claro que realmente importe.
Johan Lundberg
No me funciona. Mis funciones de destructor, constructor de movimiento y asignación de movimiento están marcadas noexcepttanto en el encabezado como en la implementación, y cuando hago un push_back (std:; move) todavía llama al constructor de copia. Me estoy arrancando el pelo aquí.
AlastairG
1
@Johan encontré el problema. Estaba usando std::move()en la push_back()llamada equivocada . Uno de esos momentos en los que busca un problema con tanta atención que no ve el error obvio justo frente a usted. Y luego llegó la hora del almuerzo y olvidé borrar mi comentario.
AlastairG
17

Curiosamente, el vector de gcc 4.7.2 solo usa el constructor de movimiento si tanto el constructor de movimiento como el destructor lo son noexcept. Un simple ejemplo:

struct foo {
    foo() {}
    foo( const foo & ) noexcept { std::cout << "copy\n"; }
    foo( foo && ) noexcept { std::cout << "move\n"; }
    ~foo() noexcept {}
};

int main() {
    std::vector< foo > v;
    for ( int i = 0; i < 3; ++i ) v.emplace_back();
}

Esto da como resultado lo esperado:

move
move
move

Sin embargo, cuando elimino noexceptde ~foo(), el resultado es diferente:

copy
copy
copy

Supongo que esto también responde a esta pregunta .

Nikola Benes
fuente
Me parece que las otras respuestas solo hablan del constructor de movimientos, no de que el destructor tenga que ser no excepto.
Nikola Benes
Bueno, debería serlo, pero resulta que en gcc 4.7.2 no lo era. Así que este problema era, de hecho, específico de gcc. Sin embargo, debería arreglarse en gcc 4.8.0. Consulte la pregunta relacionada con stackoverflow .
Nikola Benes
-1

Parece que la única forma (para C ++ 17 y versiones anteriores) de hacer cumplir la std::vectorsemántica de uso de movimiento en la reasignación es eliminar el constructor de copia :). De esta manera, usará sus constructores de movimientos o morirá en el intento, en tiempo de compilación :).

Hay muchas reglas en las que std::vectorNO DEBE usar move constructor en la reasignación, pero nada sobre dónde DEBE USARLO .

template<class T>
class move_only : public T{
public:
   move_only(){}
   move_only(const move_only&) = delete;
   move_only(move_only&&) noexcept {};
   ~move_only() noexcept {};

   using T::T;   
};

En Vivo

o

template<class T>
struct move_only{
   T value;

   template<class Arg, class ...Args, typename = std::enable_if_t<
            !std::is_same_v<move_only<T>&&, Arg >
            && !std::is_same_v<const move_only<T>&, Arg >
    >>
   move_only(Arg&& arg, Args&&... args)
      :value(std::forward<Arg>(arg), std::forward<Args>(args)...)
   {}

   move_only(){}
   move_only(const move_only&) = delete;   
   move_only(move_only&& other) noexcept : value(std::move(other.value)) {};    
   ~move_only() noexcept {};   
};

Código en vivo

Su Tclase debe tener un noexceptconstructor de movimiento / operador de asignación y un noexceptdestructor. De lo contrario, obtendrá un error de compilación.

std::vector<move_only<MyClass>> vec;
tower120
fuente
1
No es necesario eliminar el constructor de copias. Si move constructor es no, excepto, se utilizará.
balki
@balki PUEDE ser utilizado. Estándar no REQUIERE esto ahora. Aquí está la discusión groups.google.com/a/isocpp.org/forum/…
tower120