1
votes

Pouvez-vous appeler le destructeur sans appeler le constructeur?

J'ai essayé de ne pas initialiser la mémoire quand je n'en ai pas besoin, et j'utilise des tableaux malloc pour le faire:

Voici ce que j'ai exécuté:

0 ->- 0
0 ->- 1
0 ->- 2
Destroyed: 0
Destroyed: 1
Destroyed: 2


11 commentaires

Dans 0 -> - 0 ne comptez pas sur ce premier 0 étant 0. Vous avez une dose d'UB en cours ici.


@ user4581301 Je me rends compte que c'est vraiment UB -> - 0, et UB -> - 1 et UB -> - 2. Ce n'est pas un problème.


test * array = (test *) malloc (3 * sizeof (test)); - Vous n'avez rien construit ici. Aucun objet n'existe.


Consultez cette réponse . Le constructeur n'est pas appelé si vous utilisez malloc () , uniquement si vous utilisez new .


@PaulMcKenzie Je suis au courant. J'ai spécifiquement déclaré dans le post que je suis conscient de ce fait. J'ai même "le placement nouveau n'est pas utilisé" affiché comme un commentaire au cas où. Ce n'est pas ce que je demande.


La seule raison pour laquelle vous êtes allé aussi loin dans votre code est que la syntaxe C ++ vous permet de faire ce que vous faites. Il est toujours invalide.


Si vous ne traitez que des types POD, alors ce n'est pas différent de l'allocation (et de la désallocation) de struct C: vous n'avez pas besoin d'avoir et d'utiliser des constructeurs ou des destructeurs. Mais si votre struct a des membres qui ont aussi des constructeurs et des destructeurs, alors vous voudrez utiliser correctement le constructeur et destructor sur votre classe contenante aussi; vous ne voulez vraiment pas avoir à vous occuper de la construction et de la destruction de chaque membre (et de l'ordre, des exceptions, etc.). Quoi qu'il en soit, cela n'a pas de sens de faire l'un sans l'autre.


Si vous voulez "éviter la lenteur" dans cet exemple, changez int num = 3; en int num; et n'attribuez le 3 que dans les cas où vous voulez l'affectation. (Et ne lisez pas à partir de num si vous ne lui avez pas encore donné de valeur).


@jamesdlin cela n'a pas de sens dans ce contexte car je fais num = i, mais dans un cas où je ne l'initialise tout simplement pas au moment de la construction (car ce n'est pas encore nécessaire) et je le détruit plus tard (après l'avoir modifié quand il en a besoin) être changé), ce serait bénéfique.


@ ZeroZ30o Non, cela n'a jamais de sens d'utiliser un destructeur sans constructeur. Utilisez les deux ou aucun d'entre eux. Si vous voulez construire et détruire des objets paresseusement , faites-le directement sans ces autres manigances.


Vous ne pouvez pas modifier un objet non-POD ni même l'assigner avant de le construire. Si vous souhaitez lui attribuer une valeur plus tard, vous utiliserez le constructeur de copie.


4 Réponses :


6
votes

Non, ce comportement n'est pas défini. La durée de vie d'un objet commence après la fin de l'appel à un constructeur, donc si un constructeur n'est jamais appelé, l'objet n'existe techniquement jamais.

Cela "semble" se comporter correctement dans votre exemple car votre structure est triviale (int :: ~ int est un no-op).

Vous perdez également de la mémoire (les destructeurs détruisent l'objet donné, mais la mémoire d'origine allouée via malloc doit encore être libre d).

Modifier: vous voudrez peut-être regarder cette question ainsi, comme c'est une situation extrêmement similaire, en utilisant simplement l'allocation de pile au lieu de malloc . Cela donne quelques-unes des citations réelles de la norme concernant la durée de vie et la construction des objets.

Je vais également ajouter ceci: dans le cas où vous n'utilisez pas le placement new et qu'il est clairement requis (par exemple, struct contient une classe de conteneur ou une vtable, etc.), vous allez avoir de vrais problèmes. Dans ce cas, le fait d'omettre le nouvel appel de placement ne vous rapportera presque certainement aucun avantage en termes de performances pour un code très fragile.


0 commentaires

2
votes

Oui, le destructeur n'est rien de plus qu'une fonction. Vous pouvez l'appeler à tout moment. Cependant, l'appeler sans constructeur correspondant est une mauvaise idée.

La règle est donc la suivante: Si vous n'avez pas initialisé la mémoire en tant que type spécifique, vous ne pouvez pas interpréter et utiliser cette mémoire comme un objet de ce type; sinon c'est un comportement indéfini. (avec char et unsigned char comme exceptions).

Faisons une analyse ligne par ligne de votre code.

(array + i)->~test();

Cette ligne initialise un tableau scalaire de pointeur en utilisant une adresse mémoire fournie par le système. Notez que la mémoire n'est pas initialisée pour tout type de type . Cela signifie que vous ne devez pas traiter ces mémoires comme des objets (même en tant que scalaires comme int , laissez de côté votre type de classe test ). P >

Plus tard, vous avez écrit:

std::cout << array[i].num << "\n";

Ceci utilise la mémoire comme type test , ce qui enfreint la règle énoncée ci-dessus, conduisant à un comportement non défini .

Et plus tard:

test* array = (test*)malloc(3 * sizeof(test));

Vous avez à nouveau utilisé la mémoire de type test ! L'appel de destructor utilise également l'objet! C'est aussi UB.

Dans votre cas, vous avez de la chance que rien de nuisible ne se passe et que vous obtenez quelque chose de raisonnable. Cependant, les UB dépendent uniquement de l'implémentation de votre compilateur. Il peut même décider de formater votre disque et c'est toujours conforme à la norme.


4 commentaires

Notez que le comportement non défini commence par std :: cout << array [i] .num << "\ n" (vous ne pouvez pas essayer d'accéder à un objet qui n'existe pas); l'appel du destructeur est juste la cerise sur le gâteau pour ainsi dire


Je connais la sécurité et les instructions "injections", mon argument est plus "alors que l'objet n'est pas construit (UB) il est ensuite correctement initialisé avant utilisation (comme si je l'avais construit depuis le début).


@ ZeroZ30o Tant que vous utilisez l'objet après sa construction, tout va bien. Mais ne l'utilisez pas s'il n'est peut-être pas construit.


@ ZeroZ30o Comment l'initialisez-vous alors? Si votre objet avait quelque chose comme un vecteur comme membre, essayer de l'assigner au vecteur sans le construire serait UB. En outre, vous devez comprendre que UB n'est pas seulement une valeur aléatoire. Invoquer un comportement non défini annule l'intégralité de votre programme. Si vous essayez de lire quelque chose de non initialisé, tout peut arriver, y compris beaucoup de merde bizarre en plus d'obtenir simplement des valeurs inutiles.



1
votes

C'est-à-dire, puis-je simplement traiter le destructeur comme "juste une fonction"?

Non. Bien que cela ressemble à d'autres fonctions à bien des égards, il existe certaines caractéristiques spéciales du destructeur. Celles-ci se résument à un modèle similaire à la gestion manuelle de la mémoire. Tout comme l'allocation de mémoire et la désallocation doivent se faire par paires, il en va de même pour la construction et la destruction. Si vous en sautez un, sautez l’autre. Si vous appelez l'un, appelez l'autre. Si vous insistez sur la gestion manuelle de la mémoire, les outils de construction et de destruction sont placement nouveau < / a> et en appelant explicitement le destructeur. (Le code qui utilise new et delete combine l'allocation et la construction en une seule étape, tandis que la destruction et la désallocation sont combinées dans l'autre.)

N'ignorez pas le constructeur d'un objet qui sera utilisé. C'est un comportement indéfini. De plus, moins le constructeur est trivial, plus il est probable que quelque chose se passe complètement mal si vous l'ignorez. Autrement dit, au fur et à mesure que vous économisez plus, vous cassez davantage. Ignorer le constructeur d'un objet utilisé n'est pas un moyen d'être plus efficace - c'est un moyen d'écrire du code cassé. Un code inefficace et correct l'emporte sur un code efficace qui ne fonctionne pas.

Un peu de découragement: ce type de gestion de bas niveau peut devenir un gros investissement en temps. Ne suivez cette voie que s'il existe une chance réaliste de retour sur investissement. Ne compliquez pas votre code avec des optimisations simplement pour l'optimisation. Envisagez également des alternatives plus simples qui pourraient obtenir des résultats similaires avec moins de surcharge de code. Peut-être un constructeur qui n'effectue aucune initialisation autre que le marquage de l'objet comme non initialisé? (Les détails et la faisabilité dépendent de la classe impliquée, donc dépassent le cadre de cette question.)

Un petit encouragement: Si vous pensez à la bibliothèque standard, vous devriez réaliser que votre objectif est réalisable. Je présenterais vector :: reserve comme exemple de quelque chose qui peut allouer de la mémoire sans l'initialiser.


0 commentaires

0
votes

Vous avez actuellement UB lorsque vous accédez au champ à partir d'un objet non existant.

Vous pouvez laisser le champ non initialisé en faisant un noop de constructeur. le compilateur pourrait alors facilement ne faire aucune initialisation, par exemple:

struct uninitialized_tag {};

struct uninitializable_int
{
    uninitializable_int(uninitialized_tag) {} // No initalization
    uninitializable_int(int num) : num(num) {}

    int num;
};

Démo

Pour plus de lisibilité, vous devriez probablement l'envelopper dans une classe dédiée, quelque chose comme:

struct test
{
    int num; // no = 3

    test() { std::cout << "Init\n"; } // num not initalized
    ~test() { std::cout << "Destroyed: " << num << "\n"; }
};

Démo


0 commentaires