46 votes

C++ smart pointer const correctness

J'ai quelques conteneurs dans une classe, par exemple, vector ou map qui contiennent des shared_ptr's vers des objets vivant sur le tas.

Par exemple

template <typename T>
class MyExample
{
public:

private:
    vector<shared_ptr<T> > vec_;
    map<shared_ptr<T>, int> map_;
};

Je veux avoir une interface publique de cette classe qui renvoie parfois des shared_ptrs à des objets const (par l'intermédiaire de shared_ptr<const T> ) et parfois shared_ptr<T> où je permets à l'appelant de muter les objets.

Je veux que les constantes logiques soient correctes, donc si je marque une méthode comme constante, elle ne peut pas modifier les objets du tas.

Questions :

1) Je suis confus par l'interchangeabilité des termes suivants shared_ptr<const T> y shared_ptr<T> . Lorsque quelqu'un passe un shared_ptr<const T> dans la classe, n'est-ce pas ?

  • Stockez-le comme un shared_ptr<T> ou shared_ptr<const T> à l'intérieur du conteneur ?
  • OU
  • Dois-je modifier la carte, les types de vecteurs (par exemple, insert_element( shared_ptr<const T> obj) ?

2) Est-il préférable d'instancier les classes comme suit : MyExample<const int> ? Cela semble indûment restrictif, car je ne peux jamais renvoyer un fichier shared_ptr<int> ?

39voto

Terry Mahaffey Points 7368

shared_ptr<T> y shared_ptr<const T> son no interchangeable. Ça va dans un sens - shared_ptr<T> est convertible en shared_ptr<const T> mais pas l'inverse.

Observez :

// f.cpp

#include <memory>

int main()
{
    using namespace std;

    shared_ptr<int> pint(new int(4)); // normal shared_ptr
    shared_ptr<const int> pcint = pint; // shared_ptr<const T> from shared_ptr<T>
    shared_ptr<int> pint2 = pcint; // error! comment out to compile
}

compiler via

cl /EHsc f.cpp

Vous pouvez également surcharger une fonction basée sur une constance. Vous pouvez combiner ces deux faits pour faire ce que vous voulez.

Quant à votre deuxième question, MyExample<int> a probablement plus de sens que MyExample<const int> .

13voto

Rüdiger Stevens Points 5381

Je suggère la méthodologie suivante :

template <typename T>
class MyExample
{
  private:
    vector<shared_ptr<T> > data;

  public:
    shared_ptr<const T> get(int idx) const
    {
      return data[idx];
    }
    shared_ptr<T> get(int idx)
    {
      return data[idx];
    }
    void add(shared_ptr<T> value)
    {
      data.push_back(value);
    }
};

Cela garantit la correction des constantes. Comme vous le voyez la méthode add() n'utilise pas <const T> mais <T> car vous destinez la classe à stocker des Ts et non des constants. Mais en y accédant const, vous retournez <const T> ce qui n'est pas un problème puisque shared_ptr<T> peut facilement être converti en shared_ptr<const T>. Et comme les deux méthodes get() renvoient des copies des shared_ptr's dans votre stockage interne, l'appelant ne peut pas changer accidentellement l'objet vers lequel pointent vos pointeurs internes. Tout ceci est comparable à la variante du pointeur non intelligent :

template <typename T>
class MyExamplePtr
{
  private:
    vector<T *> data;

  public:
    const T *get(int idx) const
    {
      return data[idx];
    }
    T *get(int idx)
    {
      return data[idx];
    }
    void add(T *value)
    {
      data.push_back(value);
    }
};

0 votes

Ne devrait-il pas en être ainsi shared_ptr<T> peut facilement être converti en shared_ptr<const T>` et non l'inverse ?

0 votes

Lorsque vous retournez un simple membre, la surcharge ne semble pas être un gros problème. Mais qu'en est-il si vous retournez un shared_ptr d'un vector - où vous devez calculer laquelle est la bonne. Comment éviter la duplication du code ?

6voto

SoapBox Points 14183

Si quelqu'un vous passe un shared_ptr<const T> vous ne devriez jamais être en mesure de modifier T . Il est, bien sûr, techniquement possible de couler la const T à juste un T mais cela rompt l'intention de rendre le T const . Donc, si vous voulez que les gens soient capables d'ajouter des objets à votre classe, ils devraient vous donner shared_ptr<T> et non shared_ptr<const T> . Lorsque vous retournez des éléments de votre classe que vous ne voulez pas modifier, c'est là que vous utilisez la fonction shared_ptr<const T> .

shared_ptr<T> peut être automatiquement converti (sans un cast explicite) en un fichier shared_ptr<const T> mais pas l'inverse. Il peut vous être utile (et vous devriez le faire de toute façon) de faire un usage libéral de const méthodes. Lorsque vous définissez une méthode de classe const le compilateur ne vous laissera pas modifier les membres de vos données ni renvoyer quoi que ce soit d'autre qu'une valeur de type const T . Ainsi, l'utilisation de ces méthodes vous permettra de vous assurer que vous n'avez pas oublié quelque chose, et aidera les utilisateurs de votre classe à comprendre l'intention de la méthode. (Exemple : virtual shared_ptr<const T> myGetSharedPtr(int index) const; )

Vous avez raison sur votre deuxième affirmation, vous ne voulez probablement pas instancier votre classe en tant que <const T> puisque vous ne serez jamais en mesure de modifier l'une de vos T s.

3voto

Evan Teran Points 42370

Une chose à réaliser est que :

tr1::shared_ptr<const T> imite la fonctionnalité de T const * à savoir que ce qu'il pointe est const, mais le pointeur lui-même ne l'est pas.

Vous pouvez donc attribuer une nouvelle valeur à votre pointeur partagé, mais je pense que vous ne pourrez pas utiliser le pointeur déréférencé. shared_ptr comme une valeur l.

1 votes

"l-value". Une valeur l ne doit pas nécessairement être assignable !

0voto

Daniel Trugman Points 5189

Prologue

El const modifie le comportement de std::shared_ptr de la même manière qu'elle affecte les anciens pointeurs C.

Les pointeurs intelligents doivent être gérés et stockés en utilisant à tout moment les bons qualificatifs afin de prévenir, d'appliquer et d'aider les programmeurs à les traiter correctement.

Réponses

  1. Lorsque quelqu'un passe un shared_ptr<const T> dans la classe, dois-je le stocker en tant que shared_ptr<T> ou shared_ptr<const T> à l'intérieur du vecteur et de la carte ou dois-je changer la carte, les types de vecteurs ?

Si votre API accepte un shared_ptr<const T> le contrat tacite entre l'appelant et vous-même stipule que vous n'êtes PAS autorisé à modifier l'adresse de l'appelant. T l'objet pointé par le pointeur, vous devez donc le conserver comme tel dans vos conteneurs internes, par ex. std::vector<std::shared_ptr<const T>> .

De plus, votre module ne devrait JAMAIS pouvoir/être autorisé à retourner std::shared_ptr<T> même si l'on peut y parvenir par programmation (voir ma réponse à la deuxième question pour savoir comment).

  1. Est-il préférable d'instancier les classes comme suit : MyExample<const int> ? Cela semble indûment restrictif, car je ne peux jamais renvoyer un fichier shared_ptr<int> ?

Ça dépend :

  • Si vous avez conçu votre module de manière à ce que les objets qui lui sont passés ne soient plus modifiés à l'avenir, utilisez le module const T comme type sous-jacent.

  • Si vous le faites, votre module devrait être capable de retourner des données non-constantes. T vous devez utiliser T comme type sous-jacent et vous aurez probablement deux récupérateurs différents, l'un qui renvoie des objets mutables ( std::shared_ptr<T> ) et un autre qui renvoie des objets non mutables ( std::shared_ptr<const T> ).

Et, même si j'espère que nous nous sommes mis d'accord sur le fait que vous ne devrait pas retourner std::shared_ptr<T> si vous avez un const T ou std::shared_ptr<const T> vous peut :

const T a = 10;
auto a_ptr = std::make_shared<T>(const_cast<T>(a));

auto b_const_ptr = std::make_shared<const T>();
auto b_ptr = std::const_pointer_cast<T>(b_const_ptr);

Exemple complet

Considérons l'exemple suivant qui couvre toutes les permutations possibles de const avec std::shared_ptr :

struct Obj
{
  int val = 0;
};

int main()
{
    // Type #1:
    // ------------
    // Create non-const pointer to non-const object
    std::shared_ptr<Obj> ptr1 = std::make_shared<Obj>();
    // We can change the underlying object inside the pointer
    ptr1->val = 1;
    // We can change the pointer object
    ptr1 = nullptr;

    // Type #2:
    // ------------
    // Create non-const pointer to const object
    std::shared_ptr<const Obj> ptr2 = std::make_shared<const Obj>();
    // We cannot change the underlying object inside the pointer
    ptr2->val = 3; // <-- ERROR
    // We can change the pointer object
    ptr2 = nullptr;

    // Type #3:
    // ------------
    // Create const pointer to non-const object
    const std::shared_ptr<Obj> ptr3 = std::make_shared<Obj>();
    // We can change the underlying object inside the pointer
    ptr3->val = 3;
    // We can change the pointer object
    ptr3 = nullptr; // <-- ERROR

    // Type #4:
    // ------------
    // Create const pointer to non-const object
    const std::shared_ptr<const Obj> ptr4 = std::make_shared<const Obj>();
    // We can change the underlying object inside the pointer
    ptr4->val = 4; // <-- ERROR
    // We can change the pointer object
    ptr4 = nullptr; // <-- ERROR

    // Assignments:
    // ------------
    // Conversions between objects
    // We cannot assign to ptr3 and ptr4, because they are const
    ptr1 = ptr4 // <-- ERROR, cannot convert 'const Obj' to 'Obj'
    ptr1 = ptr3;
    ptr1 = ptr2 // <-- ERROR, cannot convert 'const Obj' to 'Obj'

    ptr2 = ptr4;
    ptr2 = ptr3;
    ptr2 = ptr1;
}

Note : Ce qui suit est vrai lors de la gestion de tous les types de pointeurs intelligents. L'affectation des pointeurs peut être différente (par exemple, lors de la gestion de unique_ptr ), mais le concept est le même.

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