Pourquoi aucun constructeur de déplacement / affectation de déplacement par défaut?

89

Je suis un simple programmeur. Les variables de mes membres de classe se composent le plus souvent de types POD et de conteneurs STL. Pour cette raison, je dois rarement écrire des opérateurs d'affectation ou copier des constructeurs, car ils sont implémentés par défaut.

Ajoutez à cela, si j'utilise std::movesur des objets non mobiles, il utilise l'opérateur d'affectation, ce qui signifie qu'il std::moveest parfaitement sûr.

Comme je suis un simple programmeur, j'aimerais profiter des capacités de déplacement sans ajouter un constructeur de déplacement / opérateur d'assignation à chaque classe que j'écris, car le compilateur pourrait simplement les implémenter comme " this->member1_ = std::move(other.member1_);..."

Mais ce n'est pas le cas (du moins pas dans Visual 2010), y a-t-il une raison particulière à cela?

Plus important; y a-t-il un moyen de contourner cela?

Mise à jour: Si vous regardez la réponse de GManNickG, il fournit une excellente macro pour cela. Et si vous ne le saviez pas, si vous implémentez la sémantique de déplacement, vous pouvez supprimer la fonction membre d'échange.

Viktor Sehr
la source
5
vous savez que vous pouvez demander au compilateur de générer un cteur de déplacement par défaut
aaronman
3
std :: move n'effectue pas de déplacement, il passe simplement d'une valeur l à une valeur r. Le déplacement est toujours effectué par le constructeur de déplacement.
Owen Delahoy
1
Parlez-vous MyClass::MyClass(Myclass &&) = default;?
Sandburg
Oui, de nos jours :)
Viktor Sehr

Réponses:

76

La génération implicite de constructeurs de déplacement et d'opérateurs d'affectation a été controversée et il y a eu des révisions majeures dans les versions récentes de la norme C ++, de sorte que les compilateurs actuellement disponibles se comporteront probablement différemment en ce qui concerne la génération implicite.

Pour en savoir plus sur l'historique du problème, consultez la liste des articles du WG21 2010 et recherchez "mov"

La spécification actuelle (N3225, à partir de novembre) stipule (N3225 12.8 / 8):

Si la définition d'une classe Xne déclare pas explicitement un constructeur de déplacement, l'un sera implicitement déclaré comme étant par défaut si et seulement si

  • X n'a pas de constructeur de copie déclaré par l'utilisateur, et

  • X n'a pas d'opérateur d'affectation de copie déclaré par l'utilisateur,

  • X n'a pas d'opérateur d'affectation de déplacement déclaré par l'utilisateur,

  • X n'a pas de destructeur déclaré par l'utilisateur, et

  • le constructeur de déplacement ne serait pas implicitement défini comme supprimé.

Il existe un langage similaire dans 12.8 / 22 spécifiant quand l'opérateur d'assignation de déplacement est implicitement déclaré comme étant par défaut. Vous pouvez trouver la liste complète des modifications apportées pour prendre en charge la spécification actuelle de la génération de mouvements implicites dans N3203: resserrer les conditions de génération de mouvements implicites , qui était largement basée sur l'une des résolutions proposées par l'article de Bjarne Stroustrup N3201: Moving right along .

James McNellis
la source
4
J'ai écrit un petit article avec quelques diagrammes décrivant les relations pour le constructeur / affectation implicite (déplacer) ici: mmocny.wordpress.com/2010/12/09/implicit-move-wont-go
mmocny
Ugh donc chaque fois que je dois définir des destructeurs vides dans des classes de base polymorphes juste pour le spécifier comme virtuel, je dois également définir explicitement le constructeur de mouvement et l'opérateur d'affectation :(.
someguy
@James McNellis: C'est quelque chose que j'ai essayé auparavant, mais le compilateur ne semblait pas l'apprécier. J'allais poster le message d'erreur dans cette même réponse, mais après avoir essayé de reproduire l'erreur, je me suis rendu compte qu'il le mentionnait cannot be defaulted *in the class body*. Donc, j'ai défini le destructeur à l'extérieur et cela a fonctionné :). Je trouve cela un peu étrange, cependant. Est-ce que quelqu'un a une explication? Le compilateur est gcc 4.6.1
someguy
3
Peut-être pourrions-nous obtenir une mise à jour de cette réponse maintenant que C ++ 11 est ratifié? Curieux de savoir quels comportements ont gagné.
Joseph Garvin
2
@Guy Avraham: Je pense que ce que je disais (cela fait 7 ans), c'est que si j'ai un destructeur déclaré par l'utilisateur (même un virtuel vide), aucun constructeur de mouvement ne sera déclaré implicitement comme par défaut. Je suppose que cela aboutirait à une sémantique de copie? (Je n'ai pas touché au C ++ depuis des années.) James McNellis a ensuite commenté que cela virtual ~D() = default;devrait fonctionner tout en autorisant un constructeur de déplacement implicite.
someguy
13

Les constructeurs de déplacement générés implicitement ont été pris en compte pour la norme, mais peuvent être dangereux. Voir l' analyse de Dave Abrahams .

En fin de compte, cependant, la norme incluait la génération implicite de constructeurs de déplacement et d'opérateurs d'affectation de déplacement, bien qu'avec une liste assez importante de limitations:

Si la définition d'une classe X ne déclare pas explicitement un constructeur de déplacement, un sera implicitement déclaré comme étant par défaut si et seulement si
- X n'a ​​pas de constructeur de copie déclaré par l'utilisateur,
- X n'a ​​pas d'opérateur d'affectation de copie déclaré par l'utilisateur ,
- X n'a ​​pas d'opérateur d'affectation de déplacement déclaré par l'utilisateur,
- X n'a ​​pas de destructeur déclaré par l'utilisateur, et
- le constructeur de déplacement ne serait pas implicitement défini comme supprimé.

Ce n'est pas tout à fait tout ce qu'il y a dans l'histoire. Un ctor peut être déclaré, mais toujours défini comme supprimé:

Un constructeur de copie / déplacement déclaré implicitement est un membre public en ligne de sa classe. Un constructeur de copie / déplacement par défaut pour une classe X est défini comme supprimé (8.4.3) si X a:

- un membre variant avec un constructeur correspondant non trivial et X est une classe de type union,
- un membre de données non statique de type de classe M (ou un tableau de celui-ci) qui ne peut pas être copié / déplacé en raison de la résolution de surcharge (13.3), comme appliqué au constructeur correspondant de M, entraîne une ambiguïté ou une fonction qui est supprimée ou inaccessible du constructeur par défaut,
- une classe de base directe ou virtuelle B qui ne peut pas être copiée / déplacée car la résolution de surcharge (13.3), appliquée au constructeur correspondant de B , entraîne une ambiguïté ou une fonction qui est supprimée ou inaccessible du constructeur par défaut,
- toute classe de base directe ou virtuelle ou membre de données non statique d'un type avec un destructeur supprimé ou inaccessible du constructeur par défaut,
- pour le constructeur de copie, un membre de données non statique de type référence rvalue, ou
- pour le constructeur de déplacement, un membre de données non statique ou une classe de base directe ou virtuelle avec un type qui n'a pas de constructeur de déplacement et n'est pas trivialement copiable.

Jerry Coffin
la source
Le projet de travail actuel permet la génération de mouvements implicites dans certaines conditions et je pense que la résolution répond largement aux préoccupations d'Abrahams.
James McNellis
Je ne suis pas sûr d'avoir compris quel mouvement peut casser dans l'exemple entre Tweak 2 et Tweak 3. Pouvez-vous l'expliquer?
Matthieu M.
@Matthieu M.: Tweak 2 et Tweak 3 sont cassés, et de manière assez similaire, vraiment. Dans Tweak 2, il y a des membres privés avec des invariants qui peuvent être cassés par le cteur de mouvement. Dans Tweak 3, la classe n'a pas de membres privés elle - même , mais comme elle utilise l'héritage privé, les membres publics et protégés de la base deviennent des membres privés du dérivé, conduisant au même problème.
Jerry Coffin
1
Je ne comprenais pas vraiment comment le constructeur de mouvement briserait l'invariant de classe Tweak2. Je suppose que cela a quelque chose à voir avec le fait que le Numberserait déplacé et le vectorserait copié ... mais je ne suis pas sûr: / Je comprends que le problème se répercuterait en cascade Tweak3.
Matthieu M.
Le lien que vous avez donné semble mort?
Wolf
8

(pour l'instant, je travaille sur une macro stupide ...)

Ouais, j'ai aussi emprunté cette voie. Voici votre macro:

// detail/move_default.hpp
#ifndef UTILITY_DETAIL_MOVE_DEFAULT_HPP
#define UTILITY_DETAIL_MOVE_DEFAULT_HPP

#include <boost/preprocessor.hpp>

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE(pR, pData, pBase) pBase(std::move(pOther))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE(pR, pData, pBase) pBase::operator=(std::move(pOther));

#define UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR(pR, pData, pMember) pMember(std::move(pOther.pMember))
#define UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT(pR, pData, pMember) pMember = std::move(pOther.pMember);

#define UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        ,                                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)                                                   \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR_BASE, BOOST_PP_EMPTY, pBases))                      \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT_BASE, BOOST_PP_EMPTY, pBases)  \
                                                                                                        \
            return *this;                                                                               \
        }

#define UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)                                               \
        pT(pT&& pOther) :                                                                               \
        BOOST_PP_SEQ_ENUM(BOOST_PP_SEQ_TRANSFORM(                                                       \
            UTILITY_MOVE_DEFAULT_DETAIL_CONSTRUCTOR, BOOST_PP_EMPTY, pMembers))                         \
        {}                                                                                              \
                                                                                                        \
        pT& operator=(pT&& pOther)                                                                      \
        {                                                                                               \
            BOOST_PP_SEQ_FOR_EACH(UTILITY_MOVE_DEFAULT_DETAIL_ASSIGNMENT, BOOST_PP_EMPTY, pMembers)     \
                                                                                                        \
            return *this;                                                                               \
        }

#endif

// move_default.hpp
#ifndef UTILITY_MOVE_DEFAULT_HPP
#define UTILITY_MOVE_DEFAULT_HPP

#include "utility/detail/move_default.hpp"

// move bases and members
#define UTILITY_MOVE_DEFAULT(pT, pBases, pMembers) UTILITY_MOVE_DEFAULT_DETAIL(pT, pBases, pMembers)

// base only version
#define UTILITY_MOVE_DEFAULT_BASES(pT, pBases) UTILITY_MOVE_DEFAULT_BASES_DETAIL(pT, pBases)

// member only version
#define UTILITY_MOVE_DEFAULT_MEMBERS(pT, pMembers) UTILITY_MOVE_DEFAULT_MEMBERS_DETAIL(pT, pMembers)

#endif

(J'ai supprimé les vrais commentaires, qui sont longs et documentaires.)

Vous spécifiez les bases et / ou les membres de votre classe sous forme de liste de préprocesseurs, par exemple:

#include "move_default.hpp"

struct foo
{
    UTILITY_MOVE_DEFAULT_MEMBERS(foo, (x)(str));

    int x;
    std::string str;
};

struct bar : foo, baz
{
    UTILITY_MOVE_DEFAULT_BASES(bar, (foo)(baz));
};

struct baz : bar
{
    UTILITY_MOVE_DEFAULT(baz, (bar), (ptr));

    void* ptr;
};

Et en sort un constructeur de mouvement et un opérateur d'affectation de mouvement.

(En passant, si quelqu'un sait comment je pourrais combiner les détails en une seule macro, ce serait super.)

GManNickG
la source
Merci beaucoup, la mienne est assez similaire, sauf que j'ai dû passer le nombre de variables membres comme argument (ce qui est vraiment nul).
Viktor Sehr
1
@Viktor: Pas de problème. S'il n'est pas trop tard, je pense que vous devriez marquer l'une des autres réponses comme acceptée. Le mien était plus un "au fait, voici un moyen" et non une réponse à votre vraie question.
GManNickG
1
Si je lis correctement votre macro, dès que votre compilateur implémentera les membres de déplacement par défaut, vos exemples ci-dessus deviendront non copiables. La génération implicite de membres de copie est inhibée lorsqu'il y a des membres de déplacement explicitement déclarés présents.
Howard Hinnant
@Howard: C'est bon, c'est une solution temporaire jusque-là. :)
GManNickG
GMan: Cette macro ajoute moveconstructor \ assign si vous avez une fonction d'échange:
Viktor Sehr
4

VS2010 ne le fait pas car ils n'étaient pas standard au moment de la mise en œuvre.

Chiot
la source