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
4 Réponses :
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 a > 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.
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.
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.
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.
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; };
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"; } };
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 utiliseznew
.@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 votrestruct
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;
enint num;
et n'attribuez le 3 que dans les cas où vous voulez l'affectation. (Et ne lisez pas à partir denum
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.