56 votes

Casting dynamique pour unique_ptr

Comme c'était le cas dans Boost, C++11 fournit des fonctions pour le casting. shared_ptr :

std::static_pointer_cast
std::dynamic_pointer_cast
std::const_pointer_cast

Je me demande cependant pourquoi il n'existe pas de fonctions équivalentes pour unique_ptr .

Prenons l'exemple simple suivant :

class A { virtual ~A(); ... }
class B : public A { ... }

unique_ptr<A> pA(new B(...));

unique_ptr<A> qA = std::move(pA); // This is legal since there is no casting
unique_ptr<B> pB = std::move(pA); // This is not legal

// I would like to do something like:
// (Of course, it is not valid, but that would be the idea)
unique_ptr<B> pB = std::move(std::dynamic_pointer_cast<B>(pA));

Y a-t-il une raison pour laquelle ce modèle d'utilisation est déconseillé, et donc, des fonctions équivalentes à celles présentes dans le module shared_ptr ne sont pas prévus unique_ptr ?

4 votes

Si le cast dynamique échoue, voulez-vous que l'objet précédemment possédé soit détruit ?

2 votes

Si l'objet pointé par pA n'est pas convertible en type B (c'est-à-dire, dynamic_cast<B>(pA.get()) échoue), que voulez-vous qu'il arrive à l'objet ? Est-ce que pA conserver la propriété ? Doit-il être détruit ?

2 votes

CharlesBailey C'est en fait un bon point. Il s'agit en fait d'une décision de mise en œuvre importante. Probablement que si dynamic_cast échoue, le "bon sens" conseillerait d'abandonner le casting, sans modifier le pointeur d'origine. C'est en fait le comportement de la réponse de cdhowie.

41voto

Jonathan Wakely Points 45593

En plus de l'article de Mark Ransom réponse , a unique_ptr<X, D> peut même ne pas stocker un X* .

Si le suppresseur définit le type D::pointer alors c'est ce qui est stocké, et il se peut que ce ne soit pas un vrai pointeur, il doit seulement répondre à l'exigence de la norme de l'UE. NullablePointer et (si unique_ptr<X,D>::get() est appelé) ont un operator* qui renvoie X& mais il n'est pas nécessaire de prendre en charge le moulage vers d'autres types.

unique_ptr est assez flexible et ne se comporte pas nécessairement comme un type de pointeur intégré.

Comme demandé, voici un exemple où le type stocké n'est pas un pointeur, et où le casting n'est donc pas possible. C'est un peu artificiel, mais cela enveloppe une API de base de données inventée (définie comme une API de style C) dans une API de style C++ RAII. Le type OpaqueDbHandle répond aux critères suivants NullablePointer mais ne stocke qu'un nombre entier, qui est utilisé comme clé pour rechercher la connexion réelle à la base de données via un mappage défini par l'implémentation. Je ne présente pas ce cas comme un exemple de bonne conception, mais simplement comme un exemple d'utilisation de la fonction unique_ptr pour gérer une ressource non copiable et mobile qui n'est pas un pointeur alloué dynamiquement, où le "suppresseur" ne se contente pas d'appeler un destructeur et de désallouer la mémoire lorsque l'objet unique_ptr sort du champ d'application.

#include <memory>

// native database API
extern "C"
{
  struct Db;
  int db_query(Db*, const char*);
  Db* db_connect();
  void db_disconnect(Db*);
}

// wrapper API
class OpaqueDbHandle
{
public:
  explicit OpaqueDbHandle(int id) : id(id) { }

  OpaqueDbHandle(std::nullptr_t) { }
  OpaqueDbHandle() = default;
  OpaqueDbHandle(const OpaqueDbHandle&) = default;

  OpaqueDbHandle& operator=(const OpaqueDbHandle&) = default;
  OpaqueDbHandle& operator=(std::nullptr_t) { id = -1; return *this; }

  Db& operator*() const;

  explicit operator bool() const { return id > 0; }

  friend bool operator==(const OpaqueDbHandle& l, const OpaqueDbHandle& r)
  { return l.id == r.id; }

private:
  friend class DbDeleter;
  int id = -1;
};

inline bool operator!=(const OpaqueDbHandle& l, const OpaqueDbHandle& r)
{ return !(l == r); }

struct DbDeleter
{
  typedef OpaqueDbHandle pointer;

  void operator()(pointer p) const;
};

typedef std::unique_ptr<Db, DbDeleter> safe_db_handle;

safe_db_handle safe_connect();

int main()
{
  auto db_handle = safe_connect();
  (void) db_query(&*db_handle, "SHOW TABLES");
}

// defined in some shared library

namespace {
  std::map<int, Db*> connections;      // all active DB connections
  std::list<int> unused_connections;   // currently unused ones
  int next_id = 0;
  const unsigned cache_unused_threshold = 10;
}

Db& OpaqueDbHandle::operator*() const
{
   return connections[id];
}

safe_db_handle safe_connect()
{
  int id;
  if (!unused_connections.empty())
  {
    id = unused_connections.back();
    unused_connections.pop_back();
  }
  else
  {
    id = next_id++;
    connections[id] = db_connect();
  }
  return safe_db_handle( OpaqueDbHandle(id) );
}

void DbDeleter::operator()(DbDeleter::pointer p) const
{
  if (unused_connections.size() >= cache_unused_threshold)
  {
    db_disconnect(&*p);
    connections.erase(p.id);
  }
  else
    unused_connections.push_back(p.id);
}

0 votes

Je vous remercie pour votre réponse. Il semble vraiment que ce soit plus compliqué que ce que je pensais initialement. Pourriez-vous donner un exemple de la façon dont on construit une unique_ptr qui ne stocke pas un pointeur, s'il vous plaît ?

4 votes

+1 Je n'ai jamais pensé à utiliser unique_ptr pour mettre en œuvre une approche RAII pour une base de données :) Merci pour l'exemple.

34voto

Mark Ransom Points 132545

Les fonctions auxquelles vous faites référence font chacune un copie du pointeur. Puisque vous ne pouvez pas faire une copie d'un unique_ptr cela n'a pas de sens de lui fournir ces fonctions.

18 votes

C'est vrai, mais que faire si la seule intention est de déplacer le pointeur ? Dans ce cas, les fonctions de casting pour unique_ptr ne ferait pas de copie, mais déplacerait (ou transformerait) le pointeur.

1 votes

@betabandido : Et si le casting dynamique échoue ?

0 votes

J'ai finalement décidé d'accepter cette réponse, car même si c'est possible, il semble qu'il y ait un nombre important de problèmes lorsque l'on essaie de réduire la portée d'une unique_ptr .

14voto

cdhowie Points 62253

Pour compléter la réponse de Dave, cette fonction de modèle tentera de déplacer le contenu d'un fichier de type unique_ptr à un autre d'un type différent.

  • S'il retourne vrai, alors soit :
    • Le pointeur source était vide. Le pointeur de destination sera effacé pour répondre à la demande sémantique "déplacer le contenu de ce pointeur (rien) dans celui-là".
    • L'objet pointé par le pointeur source était convertible en type de pointeur destination. Le pointeur source sera vide, et le pointeur de destination pointera sur le même objet que celui qu'il désignait auparavant. Le pointeur de destination recevra le suppresseur du pointeur source (uniquement lors de l'utilisation de la première surcharge).
  • Si elle renvoie false, l'opération n'a pas abouti. Aucun des deux pointeurs n'aura changé d'état.

    template <typename T_SRC, typename T_DEST, typename T_DELETER> bool dynamic_pointer_move(std::unique_ptr<T_DEST, T_DELETER> & dest, std::unique_ptr<T_SRC, T_DELETER> & src) { if (!src) { dest.reset(); return true; }

    T_DEST * dest_ptr = dynamic_cast<T_DEST *>(src.get());
    if (!dest_ptr)
        return false;
    
    std::unique_ptr<T_DEST, T_DELETER> dest_temp(
        dest_ptr,
        std::move(src.get_deleter()));
    
    src.release();
    dest.swap(dest_temp);
    return true;

    }

    template <typename T_SRC, typename T_DEST> bool dynamic_pointer_move(std::unique_ptr<T_DEST> & dest, std::unique_ptr<T_SRC> & src) { if (!src) { dest.reset(); return true; }

    T_DEST * dest_ptr = dynamic_cast<T_DEST *>(src.get());
    if (!dest_ptr)
        return false;
    
    src.release();
    dest.reset(dest_ptr);
    return true;

    }

Notez que la deuxième surcharge est nécessaire pour les pointeurs déclarés std::unique_ptr<A> y std::unique_ptr<B> . La première fonction ne fonctionnera pas car le premier pointeur sera en fait de type std::unique_ptr<A, default_delete<A> > et le second de std::unique_ptr<A, default_delete<B> > ; les types de suppresseur ne seront pas compatibles et le compilateur ne vous permettra donc pas d'utiliser cette fonction.

0 votes

Donc, étant donné qu'il y a une implémentation possible, pouvez-vous penser à une raison quelconque pour laquelle le casting dynamique d'un unique_ptr serait une mauvaise pratique ? Je suis d'accord pour dire qu'utiliser move en unique_ptr n'est peut-être pas un exemple de bon codage, mais dans certaines circonstances, il peut s'avérer utile de le faire.

0 votes

Je ne peux pas dire que je trouve cela pire que de couler dynamiquement n'importe quel autre pointeur. Il y a juste le problème spécial "que se passe-t-il si le cast échoue" que vous devez résoudre (comme je le fais dans ces fonctions) lorsque vous avez affaire à des pointeurs à propriété exclusive.

0 votes

Vous pourriez vouloir vérifier un code similaire stackoverflow.com/a/26377517/2746401

7voto

Dave Points 10916

Ce n'est pas une réponse à pourquoi mais c'est une façon de le faire...

std::unique_ptr<A> x(new B);
std::unique_ptr<B> y(dynamic_cast<B*>(x.get()));
if(y)
    x.release();

Ce n'est pas tout à fait propre puisque pendant un bref moment 2 unique_ptr pensent qu'ils possèdent le même objet. Et comme cela a été commenté, vous devrez également gérer le déplacement d'un suppresseur personnalisé si vous en utilisez un (mais c'est très rare).

6 votes

C'est plus compliqué si vous avez un unique_ptr<A, Deleter> car vous devez déplacer le suppresseur.

0 votes

Bien qu'il n'y ait aucun problème à avoir deux instances temporairement : std::unique_ptr<A> x(new B) ; const auto yp = dynamic_cast<B*>(x.get()) ; std::unique_ptr<B> y(yp != nullptr ? (x.release(), yp) : nullptr) ;

4voto

Bob F Points 11

Que pensez-vous de l'approche C++11 ?

template <class T_SRC, class T_DEST>
inline std::unique_ptr<T_DEST> unique_cast(std::unique_ptr<T_SRC> &&src)
{
    if (!src) return std::unique_ptr<T_DEST>();

    // Throws a std::bad_cast() if this doesn't work out
    T_DEST *dest_ptr = &dynamic_cast<T_DEST &>(*src.get());

    src.release();
    return std::unique_ptr<T_DEST>(dest_ptr);
}

1 votes

J'aime le fait qu'il jette sur l'échec du cast (pour certains cas d'utilisation). Mais je pense que c'est une mauvaise chose qu'il renvoie toujours un pointeur NULL/empty lorsque la source est vide. Cela crée une incohérence dans l'API où vous retournez toujours NULL dans certains cas où le casting est impossible, mais pas dans d'autres. Alors peut-être : if (!src) throw std::bad_cast() ?

0 votes

Merci, très bon extrait. J'ai posté une version améliorée.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X