Question

Comme expliqué dans cette réponse, l'idiome copier-échanger est implémenté comme suit :

class MyClass
{
private:
    BigClass data;
    UnmovableClass *dataPtr;

public:
    MyClass()
      : data(), dataPtr(new UnmovableClass) { }
    MyClass(const MyClass& other)
      : data(other.data), dataPtr(new UnmovableClass(*other.dataPtr)) { }
    MyClass(MyClass&& other)
      : data(std::move(other.data)), dataPtr(other.dataPtr)
    { other.dataPtr= nullptr; }

    ~MyClass() { delete dataPtr; }

    friend void swap(MyClass& first, MyClass& second)
    {
        using std::swap;
        swap(first.data, other.data);
        swap(first.dataPtr, other.dataPtr);
    }

    MyClass& operator=(MyClass other)
    {
        swap(*this, other);
        return *this;
    }
};

En ayant une valeur MyClass comme paramètre pour Operator=, le paramètre peut être construit soit par le constructeur de copie, soit par le constructeur de déplacement.Vous pouvez ensuite extraire en toute sécurité les données du paramètre.Cela empêche la duplication de code et contribue à la sécurité des exceptions.

La réponse mentionne que vous pouvez échanger ou déplacer les variables de manière temporaire.Il traite principalement de l'échange.Cependant, un swap, s'il n'est pas optimisé par le compilateur, implique trois opérations de déplacement et, dans les cas plus complexes, effectue un travail supplémentaire supplémentaire.Quand tout ce que tu veux, c'est se déplacer le temporaire dans l'objet affecté.

Considérons cet exemple plus complexe, impliquant le modèle d'observateur.Dans cet exemple, j'ai écrit manuellement le code de l'opérateur d'affectation.L'accent est mis sur le constructeur de déplacement, l'opérateur d'affectation et la méthode d'échange :

class MyClass : Observable::IObserver
{
private:
    std::shared_ptr<Observable> observable;

public:
    MyClass(std::shared_ptr<Observable> observable) : observable(observable){ observable->registerObserver(*this); }
    MyClass(const MyClass& other) : observable(other.observable) { observable.registerObserver(*this); }
    ~MyClass() { if(observable != nullptr) { observable->unregisterObserver(*this); }}

    MyClass(MyClass&& other) : observable(std::move(other.observable))
    {
        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }

    friend void swap(MyClass& first, MyClass& second)
    {
        //Checks for nullptr and same observable omitted
            using std::swap;
            swap(first.observable, second.observable);

            second.observable->unregisterObserver(first);
            first.observable->registerObserver(first);
            first.observable->unregisterObserver(second);
            second.observable->registerObserver(second);
    }

    MyClass& operator=(MyClass other)
    {
        observable->unregisterObserver(*this);
        observable = std::move(other.observable);

        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }
}

De toute évidence, la partie dupliquée du code dans cet opérateur d'affectation écrit manuellement est identique à celle du constructeur de déplacement.Vous pourriez effectuer un échange dans l'opérateur d'affectation et le comportement serait correct, mais il effectuerait potentiellement plus de mouvements et effectuerait un enregistrement supplémentaire (dans l'échange) et une désinscription (dans le destructeur).

Ne serait-il pas beaucoup plus logique de réutiliser le code du constructeur de déplacement à la place ?

private:
    void performMoveActions(MyClass&& other)
    {
        observable->unregisterObserver(other);
        other.observable.reset(nullptr);
        observable->registerObserver(*this);
    }

public:
    MyClass(MyClass&& other) : observable(std::move(other.observable))
    {
        performMoveActions(other);
    }

    MyClass& operator=(MyClass other)
    {
        observable->unregisterObserver(*this);
        observable = std::move(other.observable);

        performMoveActions(other);
    }

Il me semble que cette approche n’est jamais inférieure à l’approche swap.Ai-je raison de penser que l'idiome copier-échanger serait mieux adapté que l'idiome copier-déplacer en C++11, ou ai-je raté quelque chose d'important ?

Était-ce utile?

La solution 3

Cela fait longtemps que j'ai posé cette question et j'ai connu la réponse pendant un moment maintenant, mais j'ai refusé d'écrire la réponse pour cela. Ici c'est.

La réponse est non. L'idiome Copy-and-Swap ne doit pas devenir l'idiome de copie et de déplacement.

Une partie importante de Copy-and-Swap (qui est également déplacée-construction-et-swap) est un moyen de mettre en œuvre des opérateurs d'affectation avec un nettoyage sécurisé. Les anciennes données sont échangées dans un temporaire construit ou construit par une copie. Lorsque l'opération est terminée, le temporaire est supprimé et son destructeur est appelé.

Le comportement de swap est là pour pouvoir réutiliser le destructeur, de sorte que vous n'ayez pas à écrire de code de nettoyage dans vos opérateurs d'affectation.

S'il n'y a pas de comportement de nettoyage à effectuer et que l'affectation uniquement, vous devriez pouvoir déclarer les opérateurs d'affectation par défaut et que la copie-et-échange n'est pas nécessaire.

Le constructeur de déplacement lui-même n'exige généralement aucun comportement de nettoyage, car c'est un nouvel objet. L'approche générale simple consiste à faire appel au constructeur de déplacement du constructeur par défaut, puis échangez tous les membres avec l'objet de déplacement. L'objet déplacé à partir sera alors comme un objet fade construit par défaut.

Toutefois, dans cette question, exemple de modèle d'observateur, c'est en fait une exception dans laquelle vous devez faire des travaux de nettoyage supplémentaires car les références à l'ancien objet doivent être modifiées. En général, je recommanderais de faire de vos observateurs et de vos observables et d'autres constructions de conception basées sur des références, sans toutefois que possible.

Autres conseils

Donnez à chaque membre spécial les soins tendres et affectueux qu’il mérite et essayez de les négliger autant que possible :

class MyClass
{
private:
    BigClass data;
    std::unique_ptr<UnmovableClass> dataPtr;

public:
    MyClass() = default;
    ~MyClass() = default;
    MyClass(const MyClass& other)
        : data(other.data)
        , dataPtr(other.dataPtr ? new UnmovableClass(*other.dataPtr)
                                : nullptr)
        { }
    MyClass& operator=(const MyClass& other)
    {
        if (this != &other)
        {
            data = other.data;
            dataPtr.reset(other.dataPtr ? new UnmovableClass(*other.dataPtr)
                                        : nullptr);
        }
        return *this;
    }
    MyClass(MyClass&&) = default;
    MyClass& operator=(MyClass&&) = default;

    friend void swap(MyClass& first, MyClass& second)
    {
        using std::swap;
        swap(first.data, second.data);
        swap(first.dataPtr, second.dataPtr);
    }
};

Le destructeur peut être implicitement défini par défaut ci-dessus si vous le souhaitez.Tout le reste doit être explicitement défini ou défini par défaut pour cet exemple.

Référence: http://accu.org/content/conf2014/Howard_Hinnant_Accu_2014.pdf

L'expression copier/échanger vous coûtera probablement des performances (voir les diapositives).Par exemple, vous êtes-vous déjà demandé pourquoi des std::types hautes performances/souvent utilisés comme std::vector et std::string vous n'utilisez pas le copier/échange ?Une mauvaise performance en est la raison.Si BigClass contient tout std::vectors ou std::strings (ce qui semble probable), votre meilleur pari est d'appeler leurs membres spéciaux parmi vos membres spéciaux.Ce qui précède explique comment procéder.

Si vous avez besoin d'une sécurité d'exception forte sur la mission, consultez les diapositives pour savoir comment l'offrir en plus des performances (recherchez « strong_assign »).

Tout d’abord, il n’est généralement pas nécessaire d’écrire un swap fonction en C++11 tant que votre classe est mobile.Le défaut swap aura recours à des mouvements :

void swap(T& left, T& right) {
    T tmp(std::move(left));
    left = std::move(right);
    right = std::move(tmp);
}

Et voilà, les éléments sont intervertis.

Deuxièmement, sur cette base, le Copy-And-Swap est toujours valable :

T& T::operator=(T const& left) {
    using std::swap;
    T tmp(left);
    swap(*this, tmp);
    return *this;
}

// Let's not forget the move-assignment operator to power down the swap.
T& T::operator=(T&&) = default;

Copiera et échangera (ce qui est un mouvement) ou déplacera et échangera (ce qui est un mouvement), et devrait toujours atteindre des performances proches de la performance optimale.Il peut y avoir quelques affectations redondantes, mais j'espère que votre compilateur s'en chargera.

MODIFIER: cela implémente uniquement l'opérateur d'affectation de copie ;un opérateur d'affectation de déplacement distinct est également requis, bien qu'il puisse être défini par défaut, sinon un débordement de pile se produira (l'affectation de déplacement et l'échange s'appelant indéfiniment).

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top