Nous avons discuté de l'idiome RAII. Certaines utilisations de langage et certains idiomes de programmation en C ++ peuvent sembler étrangers ou inutiles au premier abord, mais ils ont un but. Dans ce chapitre, nous allons explorer quelques-uns de ces usages et idiomes étranges pour comprendre d’où ils viennent et pourquoi ils sont utilisés.
Vous verrez généralement que C ++ incrémente un entier en utilisant la syntaxe ++ i au lieu de i ++. La raison en est en partie historique, en partie utile et en partie une sorte de poignée de main secrète. Vous verrez que l’un des endroits courants se trouve dans une boucle for (par exemple., pour (int i = 0; i < someNumber; ++i) …
). Pourquoi les programmeurs C ++ utilisent-ils ++ i plutôt que i ++? Voyons ce que ces deux opérateurs veulent dire.
int i = 0; int x = ++ i; int y = i ++;
Dans le code précédent, lorsque l'exécution des trois instructions sera terminée, je serai égal à 2. Mais que seront x et y égaux? Ils seront tous les deux égaux à 1. Cela s'explique par le fait que l'opérateur de pré-incrémentation dans l'instruction ++ i signifie «incrémente i et donne la nouvelle valeur de i comme résultat». Ainsi, lorsque x affecte sa valeur, i passe de 0 à 1 et la nouvelle valeur de i, 1 est attribuée à x. L'opérateur de post-incrémentation dans l'instruction i ++ signifie «incrémente i et donne la valeur initiale de i comme résultat». Ainsi, lorsque vous affectez sa valeur, i passe de 1 à 2 et la valeur d'origine de i, 1 est attribuée. jouet.
Si nous décomposions cette séquence d'instructions pas à pas, en éliminant les opérateurs de pré-incrémentation et de post-incrémentation et en les remplaçant par des ajouts réguliers, nous nous rendrions compte que pour effectuer l'affectation à y, nous avons besoin d'une variable supplémentaire. tenir la valeur initiale de i. Le résultat serait quelque chose comme ceci:
int i = 0; // int x = ++ i; i = i + 1; int x = i; // int y = i ++; int magicTemp = i; i = i + 1; int y = magicTemp;
Les premiers compilateurs, en fait, avaient l'habitude de faire des choses comme ça. Les compilateurs modernes déterminent maintenant qu'il n'y a aucun effet secondaire observable à assigner à y en premier. Le code d'assemblage qu'ils génèrent, même sans optimisation, ressemblera généralement à l'équivalent en langage assembleur de ce code C ++:
int i = 0; // int x = ++ i; i = i + 1; int x = i; // int y = i ++; int y = i; i = i + 1;
À certains égards, la syntaxe ++ i (en particulier dans une boucle for) est une restitution des débuts de C ++, et même de C avant. Sachant que d'autres programmeurs C ++ l'utilisent, l'utiliser vous-même permet aux autres de savoir que vous connaissez au moins une certaine familiarité avec les usages et le style C ++, la poignée de main secrète. La partie utile est que vous pouvez écrire une seule ligne de code, int x = ++ i;
, et obtenez le résultat que vous désirez plutôt que d'écrire deux lignes de code: i ++;
suivi par int x = i;
.
Pointe: Bien que vous puissiez enregistrer une ligne de code ici et là avec des astuces telles que la capture du résultat de l'opérateur de pré-incrémentation, il est généralement préférable d'éviter de combiner plusieurs opérations sur une seule ligne. Le compilateur ne générera pas un meilleur code, car il ne fera que décomposer cette ligne en ses composants (comme si vous aviez écrit plusieurs lignes). Par conséquent, le fournisseur générera un code machine qui exécute chaque opération de manière efficace, en respectant l'ordre des opérations et les autres contraintes de langage. Tout ce que vous ferez, c'est de confondre les autres personnes qui doivent consulter votre code. Vous introduirez également une situation idéale pour les bogues, soit parce que vous avez mal utilisé quelque chose, soit parce que quelqu'un a effectué un changement sans comprendre le code. Vous augmenterez également la probabilité que vous ne compreniez pas vous-même le code si vous y revenez six mois plus tard..
nullptr
Au début de sa vie, C ++ a adopté beaucoup de choses de C, y compris l'utilisation du zéro binaire en tant que représentation d'une valeur nulle. Cela a créé d'innombrables bugs au fil des ans. Je ne blâme pas Kernighan, Ritchie, Stroustrup ou qui que ce soit d'autre pour cela; C'est incroyable ce qu'ils ont accompli lors de la création de ces langages avec les ordinateurs disponibles dans les années 70 et au début des années 80. Essayer de comprendre quels problèmes poseront des problèmes lorsque la création d'un langage informatique est une tâche extrêmement difficile.
Néanmoins, très tôt, les programmeurs ont compris que l’utilisation d’un 0 littéral dans leur code pouvait être source de confusion dans certains cas. Par exemple, imaginez que vous avez écrit:
int * p_x = p_d; // Plus de code ici… p_x = 0;
Voulez-vous dire que le pointeur est à zéro comme écrit (c.-à-d. P_x = 0;) ou si vous vouliez définir la valeur pointée sur 0 (c.-à-d. * P_x = 0;)? Même avec un code de complexité raisonnable, le débogueur peut prendre beaucoup de temps pour diagnostiquer de telles erreurs.
Le résultat de cette réalisation a été l’adoption de la macro de préprocesseur NULL: #define NULL 0
. Cela aiderait à réduire les erreurs si vous voyiez * p_x = NULL;
ou p_x = 0;
ensuite, en supposant que vous et les autres programmeurs utilisiez la macro NULL de manière cohérente, l'erreur serait plus facile à repérer, à corriger, et le correctif, à vérifier.
Mais comme la macro NULL est une définition de préprocesseur, le compilateur ne verrait jamais autre chose que 0 en raison de la substitution textuelle; il ne pourrait pas vous avertir d'un code éventuellement erroné. Si quelqu'un redéfinissait la macro NULL sur une autre valeur, toutes sortes de problèmes supplémentaires pourraient en résulter. Redéfinir la valeur NULL est une très mauvaise chose à faire, mais parfois les programmeurs font de mauvaises choses.
C ++ 11 a ajouté un nouveau mot clé, nullptr, qui peut et doit être utilisé à la place de 0, NULL et de tout autre élément lorsque vous devez attribuer une valeur null à un pointeur ou vérifier si un pointeur est null. Il y a plusieurs bonnes raisons de l'utiliser.
Le mot clé nullptr est un mot clé de langue; il n'est pas éliminé par le préprocesseur. Puisqu'il passe au compilateur, le compilateur peut détecter des erreurs et générer des avertissements d'utilisation qu'il ne pouvait pas détecter ou générer avec le littéral 0 ou des macros..
Il ne peut pas non plus être redéfini de manière accidentelle ou intentionnelle, contrairement à une macro telle que NULL. Ceci élimine toutes les erreurs que les macros peuvent introduire.
Enfin, il offre une protection future. Avoir un zéro binaire comme valeur nulle était une décision pratique lorsqu'elle a été prise, mais c'était néanmoins arbitraire. Un autre choix raisonnable aurait peut-être été que null soit la valeur maximale d'un entier natif non signé. Il y a des avantages et des inconvénients à une telle valeur, mais rien que je sache ne l'aurait rendu inutilisable.
Avec nullptr, il devient soudainement possible de changer le comportement de null pour un environnement d’exploitation donné sans modifier le code C ++ ayant pleinement adopté nullptr. Le compilateur peut effectuer une comparaison avec nullptr, ou l'affectation de nullptr à une variable de pointeur, et générer le code machine requis par l'environnement cible. Essayer de faire la même chose avec un 0 binaire serait très difficile, voire impossible. Si, à l'avenir, quelqu'un décidait de concevoir une architecture d'ordinateur et un système d'exploitation qui ajoute un bit d'indicateur nul pour toutes les adresses de mémoire afin de désigner une valeur nulle, le C ++ moderne pourrait prendre cela en charge en raison de nullptr..
Vous verrez souvent des gens écrire du code tel que si (nullptr == p_a) …
. Je n'ai pas suivi ce style dans les exemples car cela me semble tout simplement faux. Au cours de mes 18 années d’écriture de programmes en C et C ++, je n’ai jamais eu de problème avec le problème que ce style évite. Néanmoins, d'autres personnes ont eu de tels problèmes. Ce style peut éventuellement faire partie des règles de style que vous devez suivre. donc, il vaut la peine de discuter.
Si vous avez écrit if (p_a = nullptr) …
au lieu de if (p_a == nullptr) …
, alors votre programme assignerait la valeur nulle à p_a et l'instruction if serait toujours évaluée à faux. C ++, en raison de son héritage C, vous permet d’avoir une expression qui évalue tout type entier compris entre les parenthèses d’une instruction de contrôle, telle que if. C # requiert que le résultat d'une telle expression soit une valeur booléenne. Étant donné que vous ne pouvez pas affecter de valeur à quelque chose comme nullptr ou à des valeurs constantes telles que 3 et 0,0F, si vous placez cette valeur R à gauche d'un contrôle d'égalité, le compilateur vous avertit de l'erreur. En effet, vous affecteriez une valeur à quelque chose à laquelle aucune valeur ne peut être affectée..
Pour cette raison, certains développeurs ont commencé à écrire leurs contrôles d’égalité de cette façon. L'important n'est pas le style que vous choisissez, mais que vous sachiez qu'une affectation à l'intérieur d'un élément tel qu'une expression if est valide en C ++. De cette façon, vous savez faire attention à de tels problèmes.
Quoi que vous fassiez, n’écrivez pas intentionnellement des énoncés comme si (x = 3) …
. C'est un très mauvais style, ce qui rend votre code plus difficile à comprendre et plus propice au développement de bugs..
jeter()
et noexcept (expression booléenne)
Remarque: À partir de Visual Studio 2012 RC, le compilateur Visual C ++ accepte, mais n'implémente pas les spécifications d'exception. Cependant, si vous incluez une spécification d'exception throw (), le compilateur optimisera probablement le code qu'il générerait autrement pour prendre en charge le déroulement lors du lancement d'une exception. Votre programme risque de ne pas fonctionner correctement si une exception est émise par une fonction marquée avec throw (). Les autres compilateurs qui implémentent les spécifications de projection s'attendent à ce qu'ils soient marqués correctement. Vous devez donc implémenter les spécifications d'exception appropriées si votre code doit être compilé avec un autre compilateur..
Remarque: Les spécifications d'exception utilisant la syntaxe throw () (appelées spécifications d'exception dynamique) sont obsolètes à partir de C ++ 11. En tant que tels, ils peuvent être supprimés de la langue à l'avenir. La spécification noexcept et l'opérateur remplacent cette fonctionnalité de langage, mais ne sont pas implémentés dans Visual C ++ à partir de Visual Studio 2012 RC..
Les fonctions C ++ peuvent spécifier via le mot-clé spécification d’exception throw () si oui ou non des exceptions, et si oui, de quel type lancer.
Par exemple, int AddTwoNumbers (int, int) throw ();
déclare une fonction qui, en raison des parenthèses vides, déclare qu'elle ne lève aucune exception, à l'exception de celles capturées en interne et ne le relance pas. Par contre, int AddTwoNumbers (int, int) throw (std :: logic_error);
déclare une fonction qui déclare pouvoir générer une exception de type std :: logic_error
, ou tout type dérivé de celui.
La déclaration de fonction int AddTwoNumber (int, int) lancer (…);
déclare qu'il peut lever une exception de tout type. Cette syntaxe est spécifique à Microsoft, vous devez donc l'éviter pour le code qui doit éventuellement être compilé avec autre chose que le compilateur Visual C ++..
Si aucun spécificateur n'apparaît, comme dans int AddTwoNumbers (int, int);
, alors la fonction peut lancer n'importe quel type d'exception. C'est l'équivalent d'avoir le jeter(… )
spécificateur.
C ++ 11 a ajouté la nouvelle spécification et l'opérateur noexcept (bool expression). Visual C ++ ne les prend pas en charge à partir de Visual Studio 2012 RC, mais nous en discuterons brièvement car ils seront sans aucun doute ajoutés à l'avenir..
Le spécificateur noexcept (false)
est l'équivalent des deux jeter(… )
et d'une fonction sans spécificateur de jet. Par exemple, int AddTwoNumbers (int, int) noexcept (false);
est l'équivalent des deux int AddTwoNumber (int, int) lancer (…);
et int AddTwoNumbers (int, int);
.
Les prescripteurs noexcept (true)
et noexcept sont l'équivalent de jeter()
. En d'autres termes, ils spécifient tous que la fonction ne permet à aucune exception de lui échapper.
Lors du remplacement d'une fonction membre virtuelle, la spécification d'exception de la fonction de remplacement dans la classe dérivée ne peut pas spécifier d'exceptions autres que celles déclarées pour le type qu'elle substitue. Regardons un exemple.
#comprendre#comprendre classe A public: lancer (nul) (…); virtual ~ A (void) throw (); virtual int Ajouter (int, int) throw (std :: overflow_error); float virtuel Ajouter (float, float) throw (); double virtuel ajouter (double, double) lancer (int); ; classe B: public A public: B (vide); // Bien, car ne pas lancer est le même que lancer (…). Virtual ~ B (void) throw (); // Très bien puisqu'il correspond à ~ A. // Le remplacement par l'ajout int est correct puisque vous pouvez toujours jeter moins // dans un remplacement que ce que la base dit pouvoir en lancer. virtual int Ajouter (int, int) throw () override; // La substitution d'ajout float ici n'est pas valide car la version A dit // qu'elle ne lancera pas, mais cette substitution indique qu'elle peut générer une // std :: exception. virtual float Ajouter (float, float) throw (std :: exception) override; // Le double remplacement ajouté ici est invalide car la version A // dit qu'elle peut jeter un int, mais ce remplacement dit qu'elle peut lancer un double // que la version A ne spécifie pas. double virtuel ajouter (double, double) lancer (double) neutraliser; ;
Étant donné que la syntaxe de spécification d'exception throw est obsolète, vous ne devez utiliser que sa forme parenthèses vides, throw (), afin de spécifier qu'une fonction particulière ne déclenche pas d'exceptions; sinon, laissez-le. Si vous souhaitez que les autres utilisateurs sachent quelles exceptions vos fonctions peuvent générer, envisagez d'utiliser des commentaires dans vos fichiers d'en-tête ou dans une autre documentation, en veillant à les tenir à jour..
noexcept (expression booléenne)
est également un opérateur. Lorsqu'il est utilisé en tant qu'opérateur, il prend une expression qui sera évaluée à true si elle ne peut pas lever une exception, ou false si elle peut lever une exception. Notez que le résultat est une évaluation simple. il vérifie si toutes les fonctions appelées sont noexcept (true)
, et s'il y a des instructions de projection dans l'expression. Si elle trouve des instructions throw, même celles que vous savez inaccessibles (par exemple,., si (x% 2 < 0) throw "This computer is broken";
) il peut néanmoins donner la valeur false car le compilateur n'est pas tenu d'effectuer une analyse approfondie.
L'idiome pointeur vers implémentation est une technique ancienne qui a beaucoup retenu l'attention en C ++. C'est bien parce que c'est très utile. L'essence de la technique est que, dans votre fichier d'en-tête, vous définissez l'interface publique de votre classe. Le seul membre de données que vous avez est un pointeur privé sur une classe ou une structure déclarée en aval std :: unique_ptr
pour la gestion de la mémoire exceptionnellement sûre), qui servira de mise en œuvre réelle.
Dans votre fichier de code source, vous définissez cette classe d'implémentation et toutes ses fonctions membres et ses données membres. Les fonctions publiques de l'interface appellent la classe d'implémentation pour ses fonctionnalités. Le résultat est qu'une fois que vous avez défini l'interface publique de votre classe, le fichier d'en-tête ne change jamais. Ainsi, les fichiers de code source comprenant l'en-tête n'auront pas besoin d'être recompilés en raison de changements d'implémentation qui n'affectent pas l'interface publique..
Chaque fois que vous souhaitez apporter des modifications à l'implémentation, la seule chose qui doit être recompilée est le fichier de code source contenant cette classe d'implémentation, plutôt que chaque fichier de code source incluant le fichier d'en-tête de classe..
Voici un exemple simple.
Exemple: PimplSample \ Sandwich.h
#pragma fois #includeclasse SandwichImpl; class Sandwich public: Sandwich (vide); ~ Sandwich (vide); void AddIngredient (const wchar_t * ingrédient); void RemoveIngredient (const wchar_t * ingrédient); void SetBreadType (const wchar_t * breadType); const wchar_t * GetSandwich (void); private: std :: unique_ptr m_pImpl; ;
Exemple: PimplSample \ Sandwich.cpp
#include "Sandwich.h" #include#comprendre #comprendre using namespace std; // Nous pouvons apporter les modifications souhaitées à la classe d'implémentation sans // déclencher une recompilation d'autres fichiers source incluant Sandwich.h car // SandwichImpl n'est défini que dans ce fichier source. Ainsi, seul ce fichier // source doit être recompilé si nous apportons des modifications à SandwichImpl. class SandwichImpl public: SandwichImpl (); ~ SandwichImpl (); void AddIngredient (const wchar_t * ingrédient); void RemoveIngredient (const wchar_t * ingrédient); void SetBreadType (const wchar_t * breadType); const wchar_t * GetSandwich (void); privé: vecteur m_ingredients; wstring m_breadType; wstring m_description; ; SandwichImpl :: SandwichImpl () SandwichImpl :: ~ SandwichImpl () void SandwichImpl :: AddIngredient (const wchar_t * ingrédient) m_ingredients.emplace_back (ingrédient); void SandwichImpl :: RemoveIngredient (ingrédient wchar_t *) auto it = find_if (m_ingredients.begin (), m_ingredients.end (), [=] (élément de chaîne) -> bool return (élément.compare (ingrédient) = = 0);); if (it! = m_ingredients.end ()) m_ingredients.erase (it); void SandwichImpl :: SetBreadType (const wchar_t * breadType) m_breadType = breadType; const wchar_t * SandwichImpl :: GetSandwich (void) m_description.clear (); m_description.append (L "A"); for (auto ingrédient: m_ingredients) m_description.append (ingrédient); m_description.append (L ","); m_description.erase (m_description.end () - 2, m_description.end ()); m_description.append (L "on"); m_description.append (m_breadType); m_description.append (L "."); return m_description.c_str (); Sandwich :: Sandwich (void): m_pImpl (new SandwichImpl ()) Sandwich :: ~ Sandwich (void) void Sandwich :: AddIngredient (ingr ww ingr_t *) m_pImpl-> AddIngredient (ingrédient); void Sandwich :: RemoveIngredient (const wchar_t * ingrédient) m_pImpl-> RemoveIngredient (ingrédient); void Sandwich :: SetBreadType (const wchar_t * breadType) m_pImpl-> SetBreadType (breadType); const wchar_t * Sandwich :: GetSandwich (void) return m_pImpl-> GetSandwich ();
Exemple: PimplSample \ PimplSample.cpp
#comprendre#comprendre #include "Sandwich.h" #include "… /pchar.h" en utilisant namespace std; int _pmain (int / * argc * /, _pchar * / * argv * / []) Sandwich s; s.AddIngredient (L "Turquie"); s.AddIngredient (L "Cheddar"); s.AddIngredient (L "Laitue"); s.AddIngredient (L "Tomate"); s.AddIngredient (L "Mayo"); s.RemoveIngredient (L "Cheddar"); s.SetBreadType (L "un rouleau"); wcout << s.GetSandwich() << endl; return 0;
Les bonnes pratiques et les idiomes sont essentiels pour toutes les langues ou plates-formes. Revenez donc dans cet article pour bien comprendre ce que nous avons décrit ici. Ensuite, les templates, une fonctionnalité de langage vous permettant de réutiliser votre code.
Cette leçon représente un chapitre de C ++ Succinctly, un eBook gratuit de l’équipe de Syncfusion..