Comment appliquer la sémantique de déplacement lorsqu'un vecteur se développe?

92

J'ai un std::vectorobjet d'une certaine classe A. La classe n'est pas triviale et a des constructeurs de copie et des constructeurs de déplacement définis.

std::vector<A>  myvec;

Si je remplis le vecteur avec des Aobjets (en utilisant par exemple myvec.push_back(a)), le vecteur augmentera en taille, en utilisant le constructeur de copie A( const A&)pour instancier de nouvelles copies des éléments dans le vecteur.

Puis-je en quelque sorte imposer que le constructeur de mouvement de la classe Asoit utilisé à la place?

Bertwim van Beest
la source
5
Vous pouvez, en utilisant une implémentation de vecteur sensible au mouvement.
K-ballo
2
Pouvez-vous être un peu plus précis pour y parvenir?
Bertwim van Beest
1
Vous utilisez simplement une implémentation vectorielle sensible au mouvement. Il semble que votre implémentation de bibliothèque standard (qu'est-ce que c'est?) Ne prend pas en charge les déplacements. Vous pouvez essayer avec des conteneurs sensibles aux mouvements de Boost.
K-ballo
1
Eh bien, j'utilise gcc 4.5.1, qui prend en charge les mouvements.
Bertwim van Beest
Dans mon code, cela a fonctionné pour rendre le constructeur de copie privé, même si le constructeur de déplacement n'avait pas le "noexcept" explicite.
Arne

Réponses:

126

Vous devez informer C ++ (spécifiquement std::vector) que votre constructeur et destructeur de déplacement ne lance pas, en utilisant noexcept. Ensuite, le constructeur de déplacement sera appelé lorsque le vecteur se développera.

Voici comment déclarer et implémenter un constuctor de déplacement qui est respecté par 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 le constructeur ne l'est pas noexcept, ne std::vectorpeut pas l'utiliser, car il ne peut pas garantir les garanties d'exception exigées par le standard.

Pour en savoir plus sur ce qui est dit dans la norme, lisez la sémantique et les exceptions C ++ Move

Merci à Bo qui a laissé entendre que cela pourrait avoir à voir avec des exceptions. Tenez également compte des conseils de Kerrek SB et utilisez-les emplace_backsi possible. Cela peut être plus rapide (mais ce n'est souvent pas le cas), cela peut être plus clair et plus compact, mais il y a aussi quelques pièges (surtout avec les constructeurs non explicites).

Modifier , souvent la valeur par défaut est ce que vous voulez: déplacez tout ce qui peut être déplacé, copiez le reste. Pour demander cela explicitement, écrivez

A(A && rhs) = default;

En faisant cela, vous obtiendrez noexcept lorsque cela est possible: le constructeur Move par défaut est-il défini comme noexcept?

Notez que les premières versions de Visual Studio 2015 et les versions antérieures ne le prenaient pas en charge, même s'il prend en charge la sémantique de déplacement.

Johan Lundberg
la source
Sur intérêt, comment ne l'impl « savoir » si le value_typel » cteur move est noexcept? Peut-être que le langage restreint l'ensemble de candidats à l'appel de fonction lorsque la portée appelante est également une noexceptfonction?
Courses de légèreté en orbite le
1
@LightnessRacesinOrbit Je suppose qu'il s'agit simplement de faire quelque chose comme en.cppreference.com/w/cpp/types/is_move_constructible . Il ne peut y avoir qu'un seul constructeur de mouvement, il doit donc être clairement défini par la déclaration.
Johan Lundberg
@LightnessRacesinOrbit, j'ai appris depuis qu'il n'y a pas de moyen (standard / utile) de vraiment savoir s'il existe un noexceptconstructeur de déplacement. is_nothrow_move_constructiblesera vrai s'il existe un nothrowconstructeur de copie. Je ne connais aucun cas réel de nothrowconstructeurs de copie coûteux , il n'est donc pas clair que cela compte vraiment.
Johan Lundberg
Ça ne marche pas pour moi. Mes fonctions destructor, move constructor et move assigment sont toutes marquées noexceptà la fois dans l'en-tête et dans l'implémentation, et quand je fais un push_back (std:; move), il appelle toujours le constructeur de copie. Je me déchire les cheveux ici.
AlastairG du
1
@Johan j'ai trouvé le problème. J'utilisais std::move()sur le mauvais push_back()appel. Un de ces moments où vous cherchez si fort un problème que vous ne voyez pas l'erreur évidente juste devant vous. Et puis c'était l'heure du déjeuner et j'ai oublié de supprimer mon commentaire.
AlastairG
17

Fait intéressant, le vecteur de gcc 4.7.2 n'utilise le constructeur de déplacement que si le constructeur de déplacement et le destructeur le sont noexcept. Un exemple simple:

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();
}

Cela produit le attendu:

move
move
move

Cependant, lorsque je supprime noexceptde ~foo(), le résultat est différent:

copy
copy
copy

Je suppose que cela répond également à cette question .

Nikola Benes
la source
Il me semble que les autres réponses ne parlent que du constructeur de mouvement, et non du destructeur devant être noexcept.
Nikola Benes
Eh bien, ça devrait l'être, mais il s'avère que dans gcc 4.7.2 ce n'était pas le cas. Donc, ce problème était, en fait, spécifique à gcc. Cela devrait être corrigé dans gcc 4.8.0, cependant. Voir la question relative au stackoverflow .
Nikola Benes
-1

Il semble que le seul moyen (pour C ++ 17 et les versions antérieures) d'imposer la std::vectorsémantique use move lors de la réallocation est de supprimer le constructeur de copie :). De cette façon, il utilisera vos constructeurs de mouvement ou mourra en essayant, au moment de la compilation :).

Il existe de nombreuses règles où il std::vectorNE DOIT PAS utiliser le constructeur de déplacement lors de la réallocation, mais rien sur l'endroit où il DOIT L'UTILISER .

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;   
};

Vivre

ou

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 {};   
};

Code en direct

Votre Tclasse doit avoir noexceptun opérateur de constructeur / d'assignation de déplacement et un noexceptdestructeur. Sinon, vous obtiendrez une erreur de compilation.

std::vector<move_only<MyClass>> vec;
tour120
la source
1
Il n'est pas nécessaire de supprimer le constructeur de copie. Si le constructeur de déplacement est noexcept, il sera utilisé.
balki
@balki Il PEUT être utilisé. La norme n'exige pas cela maintenant. Voici la discussion groups.google.com/a/isocpp.org/forum/…
tower120