43 votes

Quelle est la motivation derrière le fait que la copie et l'initialisation directe se comportent différemment ?

Quelque peu lié à Pourquoi le constructeur de copie est-il appelé au lieu du constructeur de conversion ?

Il existe deux syntaxes d'initialisation, l'initialisation directe et l'initialisation par copie :

A a(b);
A a = b;

Je veux savoir ce qui les motive à avoir un comportement différent. Pour l'initialisation de la copie, une copie supplémentaire est impliquée, et je ne peux pas penser à une quelconque utilité pour cette copie. Puisqu'il s'agit d'une copie à partir d'un temp, elle peut et sera probablement optimisée, donc l'utilisateur ne peut pas s'attendre à ce que cela se produise - ergo la copie supplémentaire elle-même n'est pas une raison suffisante pour le comportement différent. Alors... pourquoi ?

4voto

Suma Points 11966

Ce n'est qu'une hypothèse, mais je crains qu'il soit difficile d'en être plus sûr sans que Bjarne Stroustrup ne confirme ce qu'il en est réellement :

Il a été conçu de cette façon parce que l'on a supposé que le programmeur s'attendrait à un tel comportement, qu'il s'attendrait à ce que la copie soit effectuée lorsque le signe = est utilisé, et non avec la syntaxe de l'initialisateur direct.

Je pense que l'élision de copie possible n'a été ajoutée que dans les versions ultérieures de la norme, mais je n'en suis pas sûr - c'est quelque chose que quelqu'un pourra certainement dire en vérifiant l'historique de la norme.

4voto

Bo Persson Points 42821

Puisqu'il s'agit d'une copie d'un temporaire, il peut et probablement sera optimisé

Le mot clé ici est probablement . La norme autorise, mais n'oblige pas, un compilateur à optimiser la copie. Si certains compilateurs autorisaient ce code (optimisé), mais que d'autres le rejetaient (non optimisé), cela serait très incohérent.

La norme prescrit donc une manière cohérente de traiter ce problème : chacun doit vérifier que le constructeur de copie est accessible, qu'il l'utilise ensuite ou non.

L'idée est que tous les compilateurs doivent soit accepter le code, soit le rejeter. Sinon, il ne sera pas portable.


Autre exemple, considérez

A a;
B b;

A a1 = a;
A a2 = b;

Il serait tout aussi incohérent de permettre a2 mais interdit a1 quand A Le constructeur de la copie est privé.


Nous pouvons également voir dans le texte de la norme que les deux méthodes d'initialisation d'un objet de classe étaient destinées à être différentes (8.5/16) :

Si l'initialisation est une initialisation directe, ou si c'est une initialisation par copie où la version cv-unqualifiée du type source est la même classe que, ou une classe dérivée de, la classe de la destination, les constructeurs sont considérés. Les constructeurs applicables sont énumérés (13.3.1.3), et le meilleur est choisi par résolution de surcharge (13.3). Le constructeur ainsi sélectionné est appelé pour initialiser l'objet, avec l'expression initialisatrice ou liste d'expressions comme argument(s). Si aucun constructeur ne s'applique, ou si la résolution de la surcharge est ambiguë, l'initialisation est mal formée.

Sinon (c'est-à-dire pour les autres cas d'initialisation par copie), les séquences de conversion définies par l'utilisateur qui peuvent convertir le type source en type destination ou (lorsqu'une fonction de conversion est utilisée) en une classe dérivée de celui-ci sont énumérées comme décrit en 13.3.1.4, et la meilleure est choisie par résolution de surcharge (13.3). Si la conversion ne peut pas être faite ou est ambiguë, l'initialisation est mal formée. La fonction sélectionnée est appelée avec l'expression d'initialisation comme argument ; si la fonction est un constructeur, l'appel initialise un temporaire de la version cv-unqualifiée du type de destination. Le temporaire est une prvalue. Le résultat de l'appel (qui est le temporaire pour le cas du constructeur) est ensuite utilisé pour initialiser directement, selon les règles ci-dessus, l'objet qui est la destination de l'initialisation par copie. Dans certains cas, une implémentation est autorisée à éliminer la copie inhérente à cette initialisation directe en construisant le résultat intermédiaire directement dans l'objet à initialiser ; voir 12.2, 12.8.

La différence est que l'initialisation directe utilise directement les constructeurs de la classe construite. Avec l'initialisation par copie, d'autres fonctions de conversion sont prises en compte et celles-ci peuvent produire un temporaire qui doit être copié.

1voto

Jesse Good Points 22971

Prenons l'exemple suivant :

struct X
{
    X(int);
    X(const X&);
};

int foo(X x){/*Do stuff*/ return 1; }
X x(1);
foo(x);

Dans les compilateurs que j'ai testés, l'argument de la fonction foo a toujours été copié même avec l'optimisation complète activée. Nous pouvons en déduire que les copies ne sera pas/ne doit pas être être éliminé dans toutes les situations.

Imaginons maintenant, du point de vue de la conception du langage, tous les scénarios auxquels il faudrait réfléchir si l'on voulait établir des règles pour déterminer quand une copie est nécessaire et quand elle ne l'est pas. Ce serait très difficile. De plus, même si vous parveniez à établir des règles, elles seraient très complexes et presque impossibles à comprendre pour les gens. Cependant, en même temps, si vous imposiez des copies partout, ce serait très inefficace. C'est pourquoi les règles sont telles qu'elles sont, elles sont compréhensibles pour les gens tout en n'obligeant pas à faire des copies si cela peut être évité.

Je dois admettre maintenant que cette réponse est très similaire à celle de Suma. L'idée est que l'on peut s'attendre à un comportement avec les règles actuelles, et que toute autre chose serait trop difficile à suivre pour les gens.

0voto

Tony Points 793

Initialisation des types intégrés comme :

int i = 2;

est une syntaxe très naturelle, en partie pour des raisons historiques (rappelez-vous vos mathématiques de lycée). Elle est plus naturelle que :

int i(2);

même si certains mathématiciens peuvent contester ce point. Après tout, il n'y a rien de contre nature à appeler une fonction (un constructeur dans ce cas) et à lui passer un argument.

Pour les types intégrés, ces deux types d'initialisation sont identiques. Il n'y a pas de copie supplémentaire dans le premier cas. C'est la raison pour laquelle il existe deux types d'initialisation et, à l'origine, il n'y avait pas d'intention spécifique de les faire se comporter différemment.

Cependant, il existe des types définis par l'utilisateur et l'un des objectifs déclarés du langage est de leur permettre de se comporter aussi étroitement que possible comme des types intégrés.

Ainsi, la construction de copies (en prenant l'entrée d'une fonction de conversion, par exemple) est l'implémentation naturelle de la première syntaxe.

Le fait que vous puissiez avoir des copies supplémentaires et qu'elles puissent être élidées est une optimisation pour les types définis par l'utilisateur. L'élision des copies et les constructeurs explicites sont apparus beaucoup plus tard dans le langage. Il n'est pas surprenant que la norme autorise les optimisations après une certaine période d'utilisation. En outre, vous pouvez maintenant éliminer les constructeurs explicites des candidats à la résolution des surcharges.

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