Comment gérer les cas d'échec dans le constructeur de classe C ++?

21

J'ai une classe CPP dont le constructeur effectue certaines opérations. Certaines de ces opérations peuvent échouer. Je sais que les constructeurs ne retournent rien.

Mes questions sont,

  1. Est-il autorisé d'effectuer d'autres opérations que l'initialisation des membres dans un constructeur?

  2. Est-il possible de dire à la fonction appelante que certaines opérations dans le constructeur ont échoué?

  3. Puis-je faire new ClassName()retourner NULL si des erreurs se produisent dans le constructeur?

MayurK
la source
22
Vous pouvez lever une exception depuis le constructeur. C'est un modèle tout à fait valable.
Andy
1
Vous devriez probablement jeter un coup d'œil à certains des modèles de création du GoF . Je recommande le modèle d'usine.
SpaceTrucker
2
Un exemple courant de # 1 est la validation des données. IE si vous avez une classe Square, avec un constructeur qui prend un paramètre, la longueur d'un côté, vous voulez vérifier si cette valeur est supérieure à 0.
David dit Reinstate Monica
1
Pour la première question, permettez-moi de vous avertir que les fonctions virtuelles peuvent se comporter de manière non intuitive dans les constructeurs. Même chose avec les déconstructeurs. Attention à appeler ça.
1
# 3 - Pourquoi voudriez-vous retourner un NULL? L'un des avantages d'OO est de ne PAS avoir à vérifier les valeurs de retour. Il suffit d'attraper () les exceptions potentielles appropriées.
MrWonderful

Réponses:

42
  1. Oui, bien que certaines normes de codage puissent l'interdire.

  2. Oui. La méthode recommandée consiste à lever une exception. Vous pouvez également stocker les informations d'erreur à l'intérieur de l'objet et fournir des méthodes pour accéder à ces informations.

  3. Non.

Sebastian Redl
la source
4
À moins que l'objet ne soit toujours dans un état valide même si une partie des arguments du constructeur ne répond pas aux exigences et est donc marquée comme une erreur, 2) n'est vraiment pas recommandé de le faire. Il est préférable qu'un objet existe dans un état valide ou n'existe pas du tout.
Andy
@DavidPacker D'accord, voir ici: stackoverflow.com/questions/77639/… Mais certaines directives de codage interdisent les exceptions, ce qui est problématique pour les constructeurs.
Sebastian Redl
D'une certaine manière, je vous ai déjà donné une note positive pour cette réponse, Sebastian. Intéressant. : D
Andy
10
@ooxi Non, ce n'est pas le cas. Votre nouveau surchargé est appelé pour allouer de la mémoire, mais l'appel au constructeur est effectué par le compilateur après le retour de votre opérateur, ce qui signifie que vous ne pouvez pas attraper l'erreur. Cela suppose que new est appelé du tout; ce n'est pas pour les objets alloués par pile, qui devraient être la plupart d'entre eux.
Sebastian Redl
1
Pour # 1, RAII est un exemple courant où faire plus dans le constructeur peut être nécessaire.
Eric
20

Vous pouvez créer une méthode statique qui effectue le calcul et retourne un objet en cas de succès ou non en cas d'échec.

Selon la façon dont cette construction de l'objet est effectuée, il peut être préférable de créer un autre objet qui permet la construction d'objets dans une méthode non statique.

Appeler indirectement un constructeur est souvent appelé une «usine».

Cela vous permettrait également de renvoyer un objet null, ce qui pourrait être une meilleure solution que de retourner null.

nul
la source
Merci @null! Malheureusement, je ne peux pas accepter deux réponses ici :( Sinon, j'aurais accepté cette réponse aussi !! Merci encore!
MayurK
@MayurK pas de soucis, la réponse acceptée n'est pas de marquer la bonne réponse, mais celle qui a fonctionné pour vous.
null
3
@null: En C ++, vous ne pouvez pas simplement revenir NULL. Par exemple, int foo() { return NULLvous renverriez en fait 0(zéro), un objet entier. En std::string foo() { return NULL; }vous auriez accidentellement appeler ce std::string::string((const char*)NULL)qui est un comportement non défini (NULL ne pointe pas vers une chaîne terminée par 0-\).
MSalters
3
std :: optional peut être loin, mais vous pouvez toujours utiliser boost :: optional si vous voulez aller dans ce sens.
Sean Burton
1
@Vld: En C ++, les objets ne sont pas limités aux types de classe. Et avec la programmation générique, il n'est pas rare de se retrouver avec des usines pour int. Par exemple, std::allocator<int>c'est une usine parfaitement saine d'esprit.
MSalters
5

@SebastianRedl a déjà donné des réponses simples et directes, mais des explications supplémentaires pourraient être utiles.

TL; DR = il existe une règle de style pour garder les constructeurs simples, il y a des raisons à cela, mais ces raisons se rapportent principalement à un style de codage historique (ou tout simplement mauvais). La gestion des exceptions dans les constructeurs est bien définie et les destructeurs seront toujours appelés pour les variables et membres locaux entièrement construits, ce qui signifie qu'il ne devrait pas y avoir de problèmes dans le code C ++ idiomatique. La règle de style persiste de toute façon, mais normalement ce n'est pas un problème - toute l'initialisation ne doit pas être dans le constructeur, et en particulier pas nécessairement ce constructeur.


C'est une règle de style courante que les constructeurs doivent faire le minimum absolu pour configurer un état valide défini. Si votre initialisation est plus complexe, elle doit être gérée en dehors du constructeur. S'il n'y a pas de valeur bon marché à initialiser que votre constructeur peut configurer, vous devez affaiblir les invarants imposés par votre classe pour en ajouter un. Par exemple, si l'allocation de stockage pour votre classe à gérer est trop coûteuse, ajoutez un état non alloué mais encore nul, car bien sûr, avoir des états spéciaux comme null n'a jamais posé de problème à personne. Ahem.

Bien que commun, certainement sous cette forme extrême, il est très loin d'être absolu. En particulier, comme mon sarcasme l'indique, je suis dans le camp qui dit que l'affaiblissement des invariants est presque toujours un prix trop élevé. Cependant, il existe des raisons derrière la règle de style, et il existe des moyens d'avoir à la fois des constructeurs minimaux et des invariants forts.

Les raisons sont liées au nettoyage automatique des destructeurs, en particulier face aux exceptions. Fondamentalement, il doit y avoir un point bien défini lorsque le compilateur devient responsable de l'appel des destructeurs. Pendant que vous êtes encore dans un appel constructeur, l'objet n'est pas nécessairement entièrement construit, il n'est donc pas valide d'appeler le destructeur pour cet objet. Par conséquent, la responsabilité de la destruction de l'objet n'est transférée au compilateur que lorsque le constructeur réussit. Ceci est connu sous le nom RAII (Resource Allocation Is Initialization) qui n'est pas vraiment le meilleur nom.

Si une levée d'exception se produit à l'intérieur du constructeur, tout ce qui est partiellement construit doit être explicitement nettoyé, généralement dans a try .. catch.

Cependant, les composants de l'objet qui ont déjà été construits avec succès sont déjà la responsabilité des compilateurs. Cela signifie qu'en pratique, ce n'est pas vraiment un gros problème. par exemple

classname (args) : base1 (args), member2 (args), member3 (args)
{
}

Le corps de ce constructeur est vide. Tant que les constructeurs sont base1, member2et member3sont exceptionnellement sûrs, il n'y a rien à craindre. Par exemple, si le constructeur de member2lancers, ce constructeur est responsable de se nettoyer. La base base1était déjà complètement construite, donc son destructeur sera automatiquement appelé. member3n'a jamais été même partiellement construit, il n'a donc pas besoin d'être nettoyé.

Même quand il y a un corps, les variables locales qui ont été entièrement construites avant que l'exception ne soit levée seront automatiquement détruites, comme toute autre fonction. Les corps de constructeurs qui jonglent avec des pointeurs bruts ou qui "possèdent" une sorte d'état implicite (stockés ailleurs) - ce qui signifie généralement qu'un appel de fonction de début / acquisition doit être mis en correspondance avec un appel de fin / libération - peuvent provoquer des problèmes de sécurité exceptionnels, mais le vrai problème là-bas ne parvient pas à gérer correctement une ressource via une classe. Par exemple, si vous remplacez des pointeurs bruts par unique_ptrdans le constructeur, le destructeur de unique_ptrsera appelé automatiquement si nécessaire.

Il y a encore d'autres raisons pour lesquelles les gens préfèrent les constructeurs faisant le minimum. L'une est simplement parce que la règle de style existe, beaucoup de gens supposent que les appels de constructeur sont bon marché. Une façon d'avoir cela, mais d'avoir toujours des invariants forts, est d'avoir une classe d'usine / constructeur distincte qui a les invariants affaiblis à la place, et qui définit la valeur initiale nécessaire en utilisant (potentiellement beaucoup) des appels de fonction membre normaux. Une fois que vous avez l'état initial dont vous avez besoin, passez cet objet comme argument au constructeur de la classe avec les invariants forts. Cela peut "voler les tripes" de l'objet à invariants faibles - déplacer la sémantique - ce qui est une opération bon marché (et généralement noexcept).

Et bien sûr, vous pouvez envelopper cela dans une make_whatever ()fonction, afin que les appelants de cette fonction n'aient jamais besoin de voir l'instance de classe invariants affaiblis.

Steve314
la source
Le paragraphe où vous écrivez "Pendant que vous êtes encore dans un appel de constructeur, l'objet n'est pas nécessairement entièrement construit, donc il n'est pas valide d'appeler le destructeur pour cet objet. Par conséquent, la responsabilité de détruire cet objet ne se transfère qu'au compilateur lorsque le constructeur termine avec succès "pourrait vraiment utiliser une mise à jour concernant la délégation des constructeurs. L'objet est entièrement construit lorsque tout constructeur le plus dérivé est terminé, et le destructeur sera appelé si une exception se produit à l'intérieur d'un constructeur délégué.
Ben Voigt
Ainsi, le constructeur "do-the-minimum" peut être privé, et la fonction "make_wwhat ()" peut être un autre constructeur qui appelle le privé.
Ben Voigt
Ce n'est pas la définition de RAII que je connais. Ma compréhension de RAII est d'acquérir intentionnellement une ressource dans (et seulement dans) le constructeur d'un objet et de la libérer dans son destructeur. De cette façon, l'objet peut être utilisé sur la pile pour gérer automatiquement l'acquisition et la libération des ressources qu'il encapsule. L'exemple classique est un verrou qui acquiert un mutex lors de sa construction et le libère lors de la destruction.
Eric
1
@Eric - Oui, c'est une pratique absolument standard - une pratique standard qui est communément appelée RAII. Ce n'est pas seulement moi qui étire la définition - c'est même Stroustrup, dans certaines discussions. Oui, RAII consiste à relier les cycles de vie des ressources aux cycles de vie des objets, le modèle mental étant la propriété.
Steve314
1
@Eric - les réponses précédentes ont été supprimées car elles étaient mal expliquées. Quoi qu'il en soit, les objets eux-mêmes sont des ressources qui peuvent être possédées. Tout doit avoir un propriétaire, dans une chaîne jusqu'à la mainfonction ou aux variables statiques / globales. Un objet alloué à l' aide new, n'est pas détenu jusqu'à ce que vous attribuez cette responsabilité, mais des pointeurs intelligents possèdent les objets tas alloués auxquels ils font référence, et les conteneurs possèdent leurs structures de données. Les propriétaires peuvent choisir de supprimer tôt, le destructeur des propriétaires est le responsable ultime.
Steve314