6
votes

Invoque le destructeur avant que le constructeur ait fini légal?

Supposons que j'ai une classe dont le constructeur apparaît un fil qui supprime l'objet:

class foo {
public:
    foo() 
    : // initialize other data-members
    , t(std::bind(&foo::self_destruct, this)) 
    {}

private:
    // other data-members
    std::thread t;
    // no more data-members declared after this

    void self_destruct() { 
        // do some work, possibly involving other data-members
        delete this; 
    }
};


4 commentaires

Vous pouvez finir par appeler le destructeur T avant que son constructeur soit pleinement exécuté. Ce serait sûrement ub.


En ce qui concerne la résolution du problème, que diriez-vous uniquement de la construction de FOO à partir d'une fonction d'usine, qui crée un partagé_ptr vous pouvez remettre le fil? (en utilisant Shared_From_This aurait été préférable, mais que n'est pas une option )


Dans cet exemple, qu'est-ce qui est plus inquiétant, certains sont que l'objet de thread sera détruit avant de revenir de la fonction appelée (autodestruction). Ainsi, quel que soit le travail à faire après "mettre fin au fil" sera exécuté sur un objet mort, et c'est sûr de certitude (probablement un crash dans ce cas). Vous pouvez toujours contrôler votre propre implémentation de la classe pour permettre un supprimer ceci; en sécurité car vous n'accédez à aucun membre de données, mais vous ne pouvez pas "tromper" un objet de classe tiers ( std :: thread ) se détruire comme ça et s'attendre à ce que les choses soient bien élevées.


Je suggère de supprimer l'acceptation de ma réponse. Il a été accepté alors qu'il est incorrect et même si j'ai essayé de répondre pleinement, je ne suis pas sûr que ce soit totalement correct moi-même.


4 Réponses :


1
votes

supprimer ceci; fonctionne correctement dans la pratique sur la plupart des plates-formes; Certains peuvent même garantir un comportement correct en tant qu'extension spécifique à la plate-forme. Mais IIRC il n'est pas bien défini en fonction de la norme.

Le comportement que vous comptez sur est qu'il est souvent possible d'appeler une fonction de membre non statistique non virtuelle sur un objet morte, à condition que cette fonction membre n'accède pas réellement ceci . Mais ce comportement n'est pas autorisé par la norme; Il est au mieux non-portable.

SECTION 3.8P6 de la norme Le rend un comportement non défini si un objet n'est pas en direct lors d'un appel à une fonction de membre non statique:

De même, avant que la durée de vie d'un objet ait commencé mais après le stockage que l'objet occupera a été alloué ou, après la fin de la durée de vie d'un objet et avant la fin du stockage que l'objet occupé est réutilisé ou libéré, tout glvalue Cela fait référence à l'objet d'origine peut être utilisé mais uniquement de manière limitée. Pour un objet en construction ou destruction, voir 12,7. Sinon, une telle glvalue fait référence à alloué Le stockage et l'utilisation des propriétés de la glvalue qui ne dépendent pas de sa valeur est bien définie. Le programme a un comportement non défini si:

  • Une conversion de lvalue à rvalue est appliquée à une telle glvalue,
  • Le glvalue est utilisé pour accéder à un élément de données non statique ou appeler une fonction de membre non statique de l'objet, ou
  • La glvalue est implicitement convertie en une référence à un type de classe de base, ou
  • La glvalue est utilisée comme opérande d'un static_cast sauf lorsque la conversion est finalement cv char et ou CV non signé Char & , ou
  • Le Glvalue est utilisé comme opérande d'un dynamic_cast ou comme opérande de typeid .

    Pour ce cas spécifique (supprimant un objet en construction), nous trouvons à la section 5.3.5P2:

    ... dans la première alternative ( Suppr Object ), la valeur de l'opérande de Supprimer peut être une valeur de pointeur null, un pointeur sur un non-tableau Objet créé par un précédent nouvel expression ou un pointeur à une sous-observation représentant une classe de base d'un tel objet (clause 10). Sinon, le comportement n'est pas défini. Dans la deuxième alternative ( Suppry Array ), la valeur de l'opérande de Supprimer peut être une valeur de pointeur NULL ou une valeur de pointeur résultant d'un tableau précédent neuf -expression . Sinon, le comportement est indéfini.

    Cette exigence n'est pas remplie. * Ce n'est pas un objet créé , passé, par un nouvelle expression . C'est un objet étant créé (présent progressif). Et cette interprétation est prise en charge par l'affaire Array, où le pointeur doit être le résultat d'un précédent nouvelle expression ... mais le nouvelle expression n'est pas encore complètement évalué ; Ce n'est pas Précédent et il n'a pas encore de résultat.


9 commentaires

"Mais IIRC [Supprimer cela] n'est pas bien défini en fonction de la norme." Il est.


@ALF: Référence S'il vous plaît, j'aimerais examiner cette règle.


Je pense que vous pourriez mettre fin à la citation juste après le "pour un objet en construction ou la destruction, voir 12,7".


Il n'y a pas de règle spéciale pour permettre l'informatique 2 + 2 , et il n'y a pas de règle spéciale pour permettre supprimer ce . Mais il y a des règles contre l'accès des trucs dans un objet détruit, c'est ce que l'on doit éviter après un supprimer ce . La plupart des cadres d'interface graphique font supprimer ce , donc c'est une fonctionnalité assez cruciale.


@Alf: Et la règle que j'ai citée peut être interprétée pour interdire un appel à une fonction de membre non statique avec la destruction de l'objet. (Une fonction appelle-t-elle les transferts de contrôle instantanés ou la durée de l'exécution de la fonction?)


@Benvoigt Peut-être que je suis mal compris ce texte, mais il semble que cela ne parle que de glissades qui font référence à des objets qui ne sont pas en construction ni de destruction, mais l'objet en question est en construction.


@sftrabbit: J'essaie de répondre à un sujet plus large de "Puis-je permettre à un objet d'être supprimé pendant que ses membres exécutaient?" Une fois que le Delier est invoqué, l'objet n'est pas en construction, c'est mort, et cette règle s'appliquerait aussi.


@Bensvoight mais à ce point, le Glvalue a déjà été utilisé pour appeler la fonction de membre non statique, n'est-ce pas? Oui, l'exécution se produit après la destruction, mais est-ce un problème? Il ne dit que ici que appeler la fonction entraînerait un comportement indéfini.


@sftrabbit: Comme je l'ai dit dans mon commentaire précédent, quelle est la durée d'un appel? L'instruction d'appel, ou l'ensemble de l'exécution jusqu'à la (éventuellement implicite) retour est atteint?



7
votes

Tout d'abord, nous voyons qu'un objet de type foo a une initialisation non triviale car son constructeur est non trivial (§3.8 / 1):

On dit qu'un objet a une initialisation non triviale s'il s'agit d'un type de classe ou d'agrégation et que celui-ci ou un de ses membres est initialisé par un constructeur autre qu'un constructeur par défaut trivial.

Nous voyons maintenant qu'un objet de type FOO S commence après la fin du constructeur (§3.8 / 1):

La durée de vie d'un objet de type t commence lorsque:

  • Stockage avec l'alignement et la taille appropriés pour le type T est obtenu et
  • Si l'objet a une initialisation non triviale, son initialisation est terminée.

    Maintenant, c'est un comportement non défini si vous faites Supprimer sur l'objet avant la fin du constructeur si le type foo a un non -Trivial destructeur (§3.8 / 5):

    Avant que la durée de vie d'un objet ait commencé mais après le stockage que l'objet occupera a été alloué à tout pointeur [...] qui fait référence à l'emplacement de stockage où l'objet sera ou qu'il était situé peut être utilisé mais seulement dans façons limitées. Pour un objet en construction ou destruction, voir 12,7. Sinon, [...]

    Donc, depuis notre objet sous la construction, nous examinons le §12.7:

    Les fonctions membres, y compris les fonctions virtuelles (10.3), peuvent être appelées pendant la construction ou la destruction (12.6.2).

    Cela signifie que c'est bien pour Self_Destract à appeler tandis que l'objet est construit. Cependant, cette section ne dit rien spécifiquement de la destruction d'un objet alors qu'elle est construite. Donc, je suggère que nous examinons l'opération du Supprimer-expression .

    Premier, il "invoquera le destructeur (le cas échéant) pour l'objet [...] étant supprimé". Le destructeur est un cas particulier de la fonction membre, il convient donc de l'appeler. Cependant, §12.4 Destructeurs ne dit rien de savoir s'il est bien défini lorsque le destructeur est appelé pendant la construction. Pas de chance ici.

    Deuxièmement, "L'expression Supprimer-expression appellera une fonction ometlocation " et "la fonction DealLocation doit annoncer le stockage référencé par le pointeur". Encore une fois, rien n'est dit de faire cela au stockage qui est actuellement utilisé soit un objet en construction.

    Je soutiens donc que cela est un comportement indéfini par le fait que la norme ne l'a pas défini très précisément.

    Juste pour noter: la durée de vie d'un objet de type FOO extrémise lorsque l'appel des destructeurs commence, car il a un destructeur non trivial. Donc, si supprime ceci; se produit avant la fin de la construction de l'objet, sa durée de vie se termine avant de commencer . Cela joue avec le feu.


8 commentaires

Cela interdit de manière convaincante le cas en discussion, mais je pense que cela peut être invalide même avec un destructeur trivial, car la norme ne définit pas le comportement à moins qu'un objet ne vit pendant la durée d'un appel à une fonction de membre non statique.


@Benvoigt: Je ne dirais pas "de manière convaincante" ... ;-)


@ Cheersandhth.-Alf Comment puis-je vous convaincre? :(


UHM, je devais supprimer ma réponse. pas certain. Mais je pense à cet objet comme un sous-objet d'un autre. Ensuite, c'est sûrement UB de toute façon. Je ne pense pas que cette réponse abordée par cette réponse.


@sftrabbit: Ok j'ai trouvé où il se passe mal: la citation du §3.8 / 5 laisse la partie - indiquée par Ellipsis - où il est indiqué que le point de balle ne s'applique pas . Au lieu de cela pour le cas, il dirige le §12.7; Citation "Pour un objet en construction ou de destruction, voir 12.7". Donc, cette réponse n'est certainement pas :-(, mais ma propre réponse n'est pas assez bonne non plus, désolée.


-1 Ouch Réponse incorrecte (§3.8 / 5 Bullet Point ne s'applique pas) Sélectionné en tant que solution, je dois indiquer que, malheureusement, le seul moyen visible d'indiquer une incorrecture.


@ Cheersandhth.-Alf J'ai fait de mon mieux, mais je pense que cela est mal défini. J'aimerais pouvoir supprimer mon accept.


0 Support de vovot après la correction. Cela peut être le meilleur qui puisse être dit.



1
votes

officiellement, l'objet n'existe pas tant que le constructeur ait terminé avec succès. Une partie de la raison est que le constructeur peut être appelé à partir d'un constructeur de classe dérivé. Dans ce cas, vous ne voulez certainement pas détruire le sous-objet construit via un appel de destructeur explicite, et même moins invoquer ub en appelant supprimer ce sur une (partie d'a) pas entièrement construit objet.


StandardSee sur l'existence de l'objet, l'accent ajouté:

C ++ 11 §3,8 / 1 :
La Lifetime d'un objet est une propriété d'exécution de l'objet. Un objet est dit avoir une initialisation non triviale S'il est d'une classe ou d'un type d'agrégat et que celui-ci ou un de ses membres est initialisé par un constructeur autre qu'un trivial constructeur par défaut. [ Remarque: l'initialisation par une copie triviale / constructeur de déplacement est une initialisation non triviale. -end Note ] La durée de vie d'un objet de type T commence lorsque:
- Stockage avec l'alignement et la taille appropriés pour le type T est obtenu et
- Si l'objet a une initialisation non triviale, son initialisation est terminé .

Le constructeur dans ce cas est non trivial juste en étant fourni par l'utilisateur.


4 commentaires

Citations s'il vous plaît. Bravo et hth.


@LightnessRacesinorbit: J'ai ajouté les normes pertinentes que j'ai trouvées, mais je pense que c'est un cas de quelque chose d'UB à titre de ne pas être explicitement autorisé (à savoir non explicitement autorisé par §12.7). Ces cas exigent généralement beaucoup de travail à clouer. Je suis désolé, mais ma réponse ici est plus sur le niveau de renonciation à la main et je n'ai pas le temps de faire la recherche prolongée :-(


Je suis à peu près sûr que cette citation couvre.


@Benvoigt: Non, je n'ai pas dit ce que vous avez trompeusement paraphrase. Mais de toute façon, dans le cas de ce que vous pouvez faire avec un pointeur à un objet en construction, il existe un libellé vague au §3.8 / 5 pour "voir §12.7". Et dans cette section, certaines choses qui peuvent être faites (par exemple typeid et dynamic_cast ) sont explicitement répertoriés. Cela dit (qui devrait aborder votre perplexe sur la raison pour laquelle les situations sont différentes), comme je l'ai noté à Tomalak, je ne pense pas que cette réponse soit très solide, du tout. Il se peut que la norme ne soit pas complètement claire ici, auquel cas nous sommes d'interprétation ...



2
votes

i daesay, il est bien défini comme illégal (bien que cela puisse toujours travailler toujours avec certains compilateurs).

Ceci est quelque peu la même situation que "destructeurs non appelés quand une exception est jetée du constructeur".

Une expression de suppression, selon la norme, détruit un objet le plus dérivé (1.8) ou une matrice créée par une nouvelle expression (5.3.2). Avant la fin du constructeur, un objet n'est pas un objet le plus dérivé, mais un objet de son type d'ancêtre direct.

Votre classe FOO n'a pas de classe de base, il n'y a donc pas d'ancêtres, ceci n'a donc pas de type et votre objet n'est pas vraiment un objet du tout à l'époque < Code> Supprimer est appelé. Mais même s'il y avait une classe de base, l'objet serait un objet non dérivé (le rendu toujours illégal) et le mauvais constructeur serait appelé.


0 commentaires