web-dev-qa-db-fra.com

Alternatives de static_pointer_cast pour unique_ptr

Je comprends que l’utilisation de static_pointer_cast avec unique_ptr conduirait à une propriété partagée des données contenues.
En d'autres termes, ce que j'aimerais faire est:

unique_ptr<Base> foo = fooFactory();
// do something for a while
unique_ptr<Derived> bar = static_unique_pointer_cast<Derived>(foo);

Quoi qu'il en soit, cela entraîne deux unique_ptr qui ne devraient jamais exister en même temps, donc c'est tout simplement interdit.
Bien, cela a du sens, absolument, c’est pourquoi il n’existe rien de tel que static_unique_pointer_cast en effet.

Jusqu’à présent, dans les cas où je voulais stocker des pointeurs sur ces classes de base, mais que je devais aussi les convertir en classes dérivées (par exemple, imaginez un scénario impliquant l’effacement de types), j’ai utilisé shared_ptrs à cause de ce que je ' J'ai mentionné ci-dessus. 

Quoi qu'il en soit, je devinais s'il existait des alternatives à shared_ptrs pour un tel problème ou si elles étaient vraiment la meilleure solution dans ce cas.

17
skypjack

Pointeurs bruts

La solution à votre problème est d’obtenir le pointeur brut (non propriétaire) et de le lancer - puis laissez simplement le pointeur brut sortir de son domaine et laissez le unique_ptr<Base> restant construire la durée de vie de l’objet appartenant.

Comme ça:

unique_ptr<Base> foo = fooFactory();

{
    Base* tempBase = foo.get();
    Derived* tempDerived = static_cast<Derived*>(tempBase);
} //tempBase and tempDerived go out of scope here, but foo remains -> no need to delete

Unique_pointer_cast

L'autre option consiste à utiliser la fonction release() de unique_ptr pour l'envelopper dans un autre unique_ptr.

Comme ça

template<typename TO, typename FROM>
unique_ptr<TO> static_unique_pointer_cast (unique_ptr<FROM>&& old){
    return unique_ptr<TO>{static_cast<TO*>(old.release())};
    //conversion: unique_ptr<FROM>->FROM*->TO*->unique_ptr<TO>
}

unique_ptr<Base> foo = fooFactory();

unique_ptr<Derived> foo2 = static_unique_pointer_cast<Derived>(std::move(foo));

Rappelez-vous que cela invalide l'ancien pointeur foo

Référence de pointeurs bruts

Juste pour compléter la réponse, cette solution a en fait été proposée comme une petite modification des pointeurs bruts par le PO dans les commentaires.

De manière similaire à l'utilisation de pointeurs bruts, il est possible de convertir les pointeurs bruts, puis de créer une référence à partir de ceux-ci en effectuant un déréférencement. Dans ce cas, il est important de garantir que la durée de vie de la référence créée ne dépasse pas celle de unique_ptr.

Échantillon:

unique_ptr<Base> foo = fooFactory();
Derived& bar = *(static_cast<Derived*>(foo.get()));
//do not use bar after foo goes out of scope
27
Anedar

Je comprends que l’utilisation de static_pointer_cast avec unique_ptr donnerait lieu à une propriété partagée des données contenues.

Seulement si vous le définissez mal. La solution évidente serait de transférer la propriété, de sorte que l'objet source finisse par être vide.

Si vous ne souhaitez pas transférer la propriété, utilisez simplement un pointeur brut.

Ou si vous voulez deux propriétaires, utilisez shared_ptr.

Il semble que votre question ne concerne que partiellement le fonctionnement réel de la distribution et, en partie, l'absence d'une politique de propriété claire pour le pointeur. Si vous avez besoin de plusieurs propriétaires, qu'ils utilisent tous les deux le même type ou un type différent, vous ne devez pas utiliser unique_ptr.

Quoi qu'il en soit, cela aboutit à deux unique_ptr qui ne devraient jamais exister en même temps, donc c'est tout simplement interdit.
Bien sûr, cela a du sens, absolument, c’est la raison pour laquelle il n’existe en réalité rien de tel que static_unique_pointer_cast.

Non, ce n'est pas pour ça que ça n'existe pas. Cela n'existe pas parce que c'est trivial de l'écrire soi-même, si vous en avez besoin (et tant que vous lui donnez une sémantique saine de propriété unique). Sortez simplement le pointeur avec release() et lancez-le dans un autre unique_ptr. Simple et sécuritaire.

Ce n'est pas le cas pour le shared_ptr, où la solution "évidente" ne fait pas le bon choix:

shared_ptr<Derived> p2(static_cast<Derived*>(p1.get());

Cela créerait deux objets shared_ptr différents qui possèdent le même pointeur, mais ne partagent pas la propriété (c’est-à-dire qu’ils essaieraient tous deux de le supprimer, ce qui entraînerait un comportement indéfini).

Lorsque shared_ptr a été normalisé pour la première fois, il n’existait aucun moyen sûr de le faire. static_pointer_cast et les fonctions de diffusion correspondantes ont donc été définis. Ils avaient besoin d'accéder aux informations de mise en œuvre des informations de comptabilité shared_ptr pour fonctionner.

Cependant, au cours du processus de normalisation C++ 11, shared_ptr a été amélioré par l'ajout du "constructeur d'aliasing" qui vous permet de faire le casting simplement et en toute sécurité:

shared_ptr<Derived> p2(p1, static_cast<Derived*>(p1.get());

Si cette fonctionnalité avait toujours fait partie de shared_ptr, il est alors possible que static_pointer_cast n'ait jamais été défini.

7
Jonathan Wakely

Je voudrais ajouter quelque chose à la réponse précédente de Anedar qui appelle la méthode membre release() du std::unique_ptr< U > donné. Si vous souhaitez également implémenter un dynamic_pointer_cast (en plus d'un static_pointer_cast) pour convertir std::unique_ptr< U > en std::unique_ptr< T >, vous devez vous assurer que la ressource protégée par le pointeur unique est libérée correctement en cas d'échec du dynamic_cast (c'est-à-dire renvoie nullptr). Sinon, une fuite de mémoire se produit.

Code:

#include <iostream>
#include <memory>

template< typename T, typename U >
inline std::unique_ptr< T > dynamic_pointer_cast(std::unique_ptr< U > &&ptr) {
    U * const stored_ptr = ptr.release();
    T * const converted_stored_ptr = dynamic_cast< T * >(stored_ptr);
    if (converted_stored_ptr) {
        std::cout << "Cast did succeeded\n";
        return std::unique_ptr< T >(converted_stored_ptr);
    }
    else {
        std::cout << "Cast did not succeeded\n";
        ptr.reset(stored_ptr);
        return std::unique_ptr< T >();
    }
}

struct A { 
    virtual ~A() = default;
};
struct B : A {
    virtual ~B() { 
        std::cout << "B::~B\n"; 
    }
};
struct C : A {
    virtual ~C() { 
        std::cout << "C::~C\n"; 
    }
};
struct D { 
    virtual ~D() { 
        std::cout << "D::~D\n"; 
    }
};

int main() {

  std::unique_ptr< A > b(new B);
  std::unique_ptr< A > c(new C);
  std::unique_ptr< D > d(new D);

  std::unique_ptr< B > b1 = dynamic_pointer_cast< B, A >(std::move(b));
  std::unique_ptr< B > b2 = dynamic_pointer_cast< B, A >(std::move(c));
  std::unique_ptr< B > b3 = dynamic_pointer_cast< B, D >(std::move(d));
}

Sortie (commande possible):

Cast did succeeded
Cast did not succeeded
Cast did not succeeded
B::~B
D::~D
C::~C

Les destructeurs de C et D ne seront pas appelés, si on utilise:

template< typename T, typename U >
inline std::unique_ptr< T > dynamic_pointer_cast(std::unique_ptr< U > &&ptr) {
    return std::unique_ptr< T >(dynamic_cast< T * >(ptr.release()));
}
0
Matthias