2017-07-27 3 views
1

Ceci est une simplification d'un problème que j'ai rencontré dans un autre projet.Indice en dehors de la plage lors de la suppression d'un élément de l'ensemble

Dire que j'ai la classe suivante:

class MyClass { 

public: 
    MyClass() { 
     std::cout << "MyClass constructed\n"; 
     Instances().insert(this); 
    } 
    ~MyClass() { 
     std::cout << "MyClass destructed\n"; 
     Instances().erase(this); 
    } 

    static std::unordered_set<MyClass*>& Instances() { 
     static std::unordered_set<MyClass*> _instances; 
     return _instances; 
    } 

}; 

Il a une unordered_set statique qu'il utilise pour garder la trace des instances existantes de la classe. Lorsqu'une instance est construite, son adresse est ajoutée à l'ensemble; Lorsqu'une instance est détruite, son adresse est supprimée de l'ensemble.

Maintenant, j'ai une autre classe qui a une vector de shared_ptr s contenant des instances de MyClass:

struct InstanceContainer { 
    std::vector<std::shared_ptr<MyClass>> instances; 
}; 

Un point clé ici est qu'il ya une instance globale de cette classe au-dessus main. Cela semble être une partie du problème, car déclarer la classe à l'intérieur de main ne produit pas le problème.

intérieur de main, je fais ce qui suit (par exemple l'instance mondiale de InstanceContainer est appelé container):

container.instances.emplace_back(std::shared_ptr<MyClass>(new MyClass)); 

Tout va bien jusqu'à ce que le programme se termine, quand je reçois une violation d'accès de lecture ("vecteur indice hors plage ") lorsque Instances().erase(this) est exécuté dans le destructeur MyClass.

Je pensais que peut-être j'essayais d'effacer l'instance de _instances plusieurs fois (d'où le cout s) - Cependant, le contructor est seulement appelé une fois, et le destructeur est appelé seulement une fois, comme on s'y attendrait. J'ai trouvé que lorsque cela se produit, _instances.size() est égal à 0. La chose étrange est, il est égal à 0 avant tous les appels à erase. Avant que quelque chose ne soit effacé de l'ensemble, c'est vide ?! Ma théorie à ce stade est que cela a à voir avec l'ordre dans lequel les objets sont détruits à la fin du programme. Peut-être que le _instances statique est en train d'être libéré avant que le destructeur MyClass ne soit appelé.

J'espérais que quelqu'un serait en mesure de faire la lumière sur ce sujet, et confirmer si c'est ce qui se passe.

Ma solution de contournement consiste maintenant à vérifier si _instances.size() est 0 avant d'essayer d'effacer. Est-ce sûr? Si non, que puis-je faire d'autre?

Si c'est important, j'utilise MSVC. Voici un executable example.

Répondre

1

Voici ce qui se passe. Cette variable globale de type InstanceContainer est construite en premier, avant d'entrer main. La variable statique de fonction _instances est créée ultérieurement, lorsque Instances() est appelée pour la première fois.

Lors de l'arrêt du programme, les destructeurs de ces objets sont appelés dans l'ordre inverse de la construction. Par conséquent, _instances est détruit en premier, puis InstanceContainer, qui à son tour détruit son vecteur de pointeurs partagés, qui à son tour ~MyClass sur tous les objets encore dans le vecteur, qui à son tour appel _instances.erase() déjà détruit _instances.Votre programme présente alors un comportement indéfini en accédant à un objet dont la durée de vie est terminée.

Il y a plusieurs façons de contourner ce problème. Premièrement, vous pouvez vous assurer que InstanceContainer::instances est vide avant le retour de main. Aucune idée de la faisabilité, car vous n'avez jamais expliqué quel rôle joue InstanceContainer dans votre conception.

Deux, vous pouvez allouer _instances sur le tas, et une fuite juste:

static std::unordered_set<MyClass*>& Instances() { 
    static auto* _instances = new std::unordered_set<MyClass*>; 
    return *_instances; 
} 

Cela permet de garder en vie par la destruction des objets globaux.

Trois, vous pourriez mettre quelque chose comme ça avant la définition de InstanceContainer variable globale:

static int dummy = (MyClass::Instances(), 0); 

Cela garantira que _instances est créé plus tôt, et donc détruit plus tard.