30 votes

Y a-t-il des conséquences à utiliser *this pour initialiser une classe ?

Dans un petit jeu que j'écris, j'ai une classe Weapon avec deux constructeurs, l'un qui prend en compte certains paramètres pour produire une arme personnalisée, et l'autre qui récupère une arme par défaut (l'élément CHAIN_GUN ) :

Weapon::Weapon (void) {
    // Standard weapon
    *this = getWeapon(CHAIN_GUN);
    return;
}

Pregunta : Y a-t-il des conséquences négatives à utiliser *this y operator= pour initialiser une classe ?

33voto

Filip Roséen - refp Points 24995

Imaginez que quelqu'un vous demande de dessiner un tableau... le feriez-vous ?

  • dessinez d'abord votre par défaut ( 1er ) (ce visage familier que vous aimez tant),
  • puis dessinez ce que cette personne a demandé ( 2ème ),
  • seulement pour dessiner la même chose une fois de plus, mais sur la toile contenant votre par défaut ,
  • et ensuite brûler le 2ème la peinture ?

Ce billet va tenter d'expliquer pourquoi cette comparaison est pertinente.


POURQUOI EST-CE UNE MAUVAISE IDÉE ?

Je n'ai jamais vu un Constructeur par défaut mis en œuvre avec l'utilisation de la opérateur de mission et, honnêtement, ce n'est pas quelque chose que je recommanderais, ni que je soutiendrais lors d'une conférence de presse. révision du code .

Le problème majeur de ce type de code est que, par définition, nous construisons deux (au lieu d'un seul) et en appelant un fonction-membre ce qui signifie que nous construisons tous nos membres deux fois, et que nous devons ensuite copier/déplacer l'initialisation de tous les membres en appelant la fonction opérateur d'affectation .

Il n'est pas intuitif qu'en demandant la construction de 1 nous construisons 2 pour ensuite copier les valeurs de l'objet 2ème au 1er et jetez le 2ème .

Conclusion : Ne le faites pas. .

( Note : Dans un cas où Weapon a des classes de base, ce sera encore pire ;)

( Note : Un autre danger potentiel est que la fonction d'usine utilise accidentellement le constructeur par défaut, ce qui entraîne une récursion infinie qui n'a pas été attrapée lors de la compilation, comme l'a noté @. Ratchet Freat )


SOLUTION PROPOSÉE

Dans votre cas particulier, il est préférable d'utiliser une argument par défaut dans votre constructeur, comme dans l'exemple ci-dessous.

class Weapon {
public:
  Weapon(WeaponType w_type = CHAIN_GUN);
  ...
}

Weapon w1;             // w_type = CHAIN_GUN
Weapon w2 (KNOWLEDGE); // the most powerful weapon

( Note : Une alternative à ce qui précède serait d'utiliser une constructeur délégué (disponible en C++11)

16voto

Vaughn Cato Points 30511

Utiliser l'opérateur d'affectation pour implémenter un constructeur est rarement une bonne idée. Dans votre cas, par exemple, vous pourriez simplement utiliser un paramètre par défaut :

Weapon::Weapon(GunType g = CHAIN_GUN)
: // Initialize members based on g
{
}

Dans d'autres cas, vous pouvez utiliser un constructeur de délégation (avec C++11 ou plus) :

Weapon::Weapon(GunType g)
: // Initialize members based on g
{
}

Weapon::Weapon()
: Weapon(CHAIN_GUN) // Delegate to other constructor
{
}

7voto

Tony D Points 43962

Une chose à garder à l'esprit est que si operator= - ou toute fonction qu'il appelle - est virtual la version de classe dérivée ne le fera pas être invoquée. Il peut en résulter des champs non initialisés et, plus tard, un comportement non défini, mais tout dépend de vos membres de données.

Plus généralement, vos bases et vos membres de données sont garantis d'avoir été initialisés s'ils ont des constructeurs ou apparaissent dans la liste des initialisateurs (ou avec C++11 sont assignés dans la déclaration de la classe) - donc à part l'élément virtual numéro ci-dessus, operator= fonctionnera souvent sans comportement indéfini.

Si une base ou un membre a été initialisé avant operator=() est invoquée, puis la valeur initiale est écrasée avant qu'elle ne soit utilisée de toute façon, l'optimiseur peut ou non être capable de supprimer la première initialisation. Par exemple :

std::string s_;
Q* p_;
int i_;

X(const X& rhs)
  : p_(nullptr)  // have to initialise as operator= will delete
{
    // s_ is default initialised then assigned - optimiser may help
    // i_ not initialised but if operator= sets without reading, all's good
    *this = rhs;
}

Comme vous pouvez le voir, c'est un peu sujet aux erreurs, et même si vous y arrivez, quelqu'un viendra plus tard mettre à jour operator= peut ne pas vérifier si un constructeur l'utilise....

Vous pourriez vous retrouver avec une récursion infinie conduisant à un débordement de pile si getWeapon() utilise les modèles Prototype ou Flyweight et tente de copier la structure de l'entreprise. Weapon il revient.


En prenant un peu de recul, on peut se demander pourquoi getWeapon(CHAIN_GUN); existe sous cette forme. Si nous avons besoin d'une fonction qui crée une arme en fonction d'un type d'arme, à première vue, une fonction de type Weapon(Weapon_Type); semble être une option raisonnable. Cela dit, il existe de rares mais nombreux cas limites dans lesquels getWeapon pourrait retourner autre chose qu'un Weapon mais qui peut néanmoins être attribué à un objet Weapon ou peuvent être séparés pour des raisons de construction/déploiement. ....

3voto

Remy Lebeau Points 130112

Si vous avez défini une copie non = qui permet à l'opérateur d'affectation Weapon change son type après la construction, alors l'implémentation du constructeur en termes d'affectation fonctionne très bien, et c'est un bon moyen de centraliser votre code d'initialisation. Mais si un Weapon n'est pas censé changer de type après la construction, alors un fichier non copié = L'opérateur d'affectation n'a pas beaucoup de sens, et encore moins pour l'initialisation.

3voto

Arkady Points 638

Je suis sûr que oui.

Vous avez déjà créé un objet dans votre fonction "getWeapon", puis vous le copiez, ce qui peut être une opération longue. Donc, au moins vous devez essayer de bouger la sémantique.

Mais. Si à l'intérieur de "getWeapon" vous appelez un constructeur (et c'est ce que vous faites, d'une certaine manière "getWeapon" doit créer votre classe pour la renvoyer à votre opération de copie), vous créez une architecture très peu claire, quand un constructeur appelle une fonction qui appelle un autre constructeur.

Je pense que vous devez séparer l'initialisation de vos paramètres en fonctions privées, qui doivent être appelées depuis vos constructeurs comme vous le souhaitez.

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