Nouveau

Appels virtuels dans les constructeurs et destructeurs C++

Dans son livre Effective C++, Scott Meyers consacre l’item 9 aux appels virtuels dans les constructeurs et les destructeurs. Il déconseille de les utiliser.

J’ai écrit deux classes pour expérimenter les problèmes qu’il expose et je les ai testé sous Windows 7 avec MinGW. J’ai commencé par écrire une classe de base, dans laquelle le constructeur fait appel à une fonction virtuelle log(). L’objectif est de tracer la création des objets. La fonction est virtuelle pour permettre aux classes filles de personnaliser les logs, et l’appel est fait dans le constructeur de la classe mère car il est forcément appelé lors de l’instanciation d’une classe de la hiérarchie. Voici le code :

#include <iostream>

class Base
{
public:
    Base()
    {
        std::cout << "Base::Base()" << std::endl;
        log();
    }

    virtual ~Base()
    {
    }

    virtual void log()
    {
        std::cout << "Base::log()" << std::endl;
    }
};

class Derived : public Base
{
public:
    Derived()
    {
        std::cout << "Derived::Derived()" << std::endl;
    }

    void log()
    {
        std::cout << "Derived::log()" << std::endl;
    }
};

int main()
{
    Base b;
    Derived d;
}

Et voici la sortie en console, après une compilation sans warning :

Base::Base()
Base::log()
Base::Base()
Base::log()
Derived::Derived()

On constate que tout ce passe comme attendu quand on crée b mais pas franchement quand on construit d. En effet, lors de sa création, on construit d’abord la partie Base de l’objet. Pendant cette construction, l’objet n’est pas encore considéré comme étant de type Derived, mais comme étant de type Base. Sa vtable résout donc l’appel virtuel en appelant une fonction de la classe Base. Il est impossible de faire appel à Derived::log().

Que se passe t-il si la fonction était virtuelle pure ? On serait bien obligé d’aller chercher l’implémentation fournie par la classe fille, non ? Pour tester cela, on modifie la classe mère et on enlève la ligne Base b; du main() pour alléger la sortie et surtout parce on ne peut plus instancier cette classe car elle devient abstraite :

class Base
{
public:
    Base()
    {
        std::cout << "Base::Base()" << std::endl;
        log();
    }

    virtual ~Base()
    {
    }

    virtual void log() = 0;
};

Cette fois-ci, la compilation ne se passe pas bien : le compilateur émet un warning et l’édition des liens échoue :

..\src\main.cpp: In constructor 'Base::Base()':
..\src\main.cpp:9:13: warning: pure virtual 'virtual void Base::log()'
          called from constructor
[...]
C:\Users\X-pigradot\workspaceTestPGT\Experiences\Debug/../src/main.cpp:9:
          undefined reference to `Base::log()'

Il n’y a donc vraiment pas moyen d’appeler la fonction de la classe fille.

Notez au passage qu’une fonction virtuelle pure peut avoir une implémentation. Oui, oui. Cela pourrait servir à fournir une implémentation par défaut. Que se passe t-il alors ? Puisque lors de la construction de la partie Base de Derived, on ne peut qu’appeler des fonctions de la classe Base, qu’appelle t-on ? Essayons de rajouter une implémentation à Base::log() :

void Base::log()
{
    std::cout << "Base::log()" << std::endl;
}

Le warning à la compilation reste mais l’erreur d’édition des liens disparaît et on peut exécuter le code :

Base::Base()
Base::log()
Derived::Derived()

En fait, ce code « tombe en marche ». L’appel d’une méthode virtuelle pure dans un constructeur est un undefined behavior, comme l’explique Raymon Chen dans cet article C++ corner case: You can implement pure virtual functions in the base class. Ici, ça a un comportement conforme à ce qu’on pourrait attendre mais tout aurait pu arriver.

Vous allez dire « le compilateur prévient quand même, tu l’as bien cherché à ne pas écouter ses alertes ». Et vous auriez raison. À une subtilité prêt. Il faut savoir qu’il peut y avoir bien qu’un double drame n’est pas loin. Pour vous montrer cela, voici une double modification du code. La première est d’enlever l’implémentation par défaut de la fonction virtuelle pure ; la seconde est dans le constructeur pour masquer l’appel direct à cette fonction virtuelle pure en appelant une fonction non virtuelle qui appelle à son tour la fonction qu’on ne devrait pas appeler :

class Base
{
public:
    Base()
    {
        std::cout << "Base::Base()" << std::endl;
        indirection();
    }

    void indirection()
    {
        std::cout << "Base::indirection()" << std::endl;
        log();
    }

    virtual ~Base()
    {
    }

    virtual void log() = 0;
};

// NOTE: ON ENLÈVE L’IMPLÉMENTATION !

Ce code compile sans warning (premier drame) et à l’exécution, on a (deuxième drame) :

Base::Base()
Base::indirection()
pure virtual method called

This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.
terminate called without an active exception

Le programme se termine brutalement. Désolé. Remarquez, si ça crashe on s’en rend compte rapidement… mais on espère que ça crashe avant d’arriver en prod ! Ici, la ficelle est une grosse mais par le jeu de la maintenance, avec plusieurs développeurs, au fil du temps, avec des fonctions qui appellent des fonctions qui appellent d’autres fonctions, vous pouvez vous retrouver dans une situation inconfortable où votre constructeur appelle une fonction virtuelle, pure ou pas, définie ou pas.

Conclusion : méfiez-vous des appels de fonctions virtuelles dans les constructeurs (et les destructeurs, ça a le même effet). Méfiez-vous de toute fonction que vous appelez dans vos constructeurs ou vos destructeurs car elles pourraient appeler des fonction virtuelles (aujourd’hui ou demain). Enfin, si vous rendez une fonction virtuelle ou virtuelle pure, faites attention à ce quelle ne soit pas déjà appelée (directement ou indirectement) depuis un constructeur ou un destructeur.

En complément, vous pouvez lire cet article, Be Careful with Virtual Method Calls from the Constructor (and Destructor) of your Classes in C++/C#/Java!. L’auteur montre ce que je viens de vous montrer en C++ mais aussi ce qui se passe en Java et C#.

Activer la compilation multicœur dans Eclipse CDT

Mon premier article sur ce blog expliquait comment dire à make d’utiliser plusieurs cœurs lors de la compilation. Il suffit d’utiliser l’option -j pour spécifier le nombre de jobs pouvant s’exécuter en parallèle. Si vous utilisez Eclipse CDT et son builder interne, il existe aussi une option activer la compilation multicœur. Je l’ai trouvé par hasard aujourd’hui, elle est dans les propriétés du projet puis C/C++ Build et Behavior :

eclipse-compilation-multicoeur

J’ai étonné que cette option ne soit pas cochée par défaut et j’ai bien sûr immédiatement testé ça. Mon projet n’est pas encore très gros, il n’a que 78 fichiers. En l’activant, je suis passé de 31s.792 ms à 13s.114 ms pour le builder. Wouhou !

Transformer un front montant en impulsion

Pour la réalisation de mon horloge Nixie, j’ai eu besoin de transformer un créneau en impulsion. En effet, pour incrémenter les minutes ou les heures, il y a deux solutions :

  1. On appuie sur le bouton poussoir correspondant, ce qui génère un bref signal à 1, et cela incrémente le bloc des minutes (ou des heures).
  2. Le bloc précédent (les secondes dans le cas des minutes, les minutes dans le cas des heures) arrive en fin de cycle (il va « bientôt » repasser à 00) et génère un signal à un pour réaliser l’incrément du bloc suivant.

Le problème est que tout est dans le « bientôt » et le signal venant du bloc précédent peut rester longtemps à 1. Problème : il y a une porte logique OU entre ces deux signaux et c’est un front montant en sortie de cette porte qui incrémente un bloc. Si le signal du bloc précédent est à 1, la sortie du OU est donc à 1 et l’appui le bouton n’a pas d’effet ! Je me retrouvais donc avec des instants assez longs où les boutons ne servaient à rien…

La première idée est de faire une logique compliquée pour faire en sorte qu’un appui sur le bouton alors que l’autre signal est déjà à 1 génère une transition vers 0 puis une nouvelle transition vers 1 mais c’est vraiment pas une bonne idée. La bonne idée est de transformer le créneau sortant d’un bloc précédent en impulsion et de faire rentrer cette impulsion dans la porte OU. Ainsi, le temps où le bouton est sans effet est négligeable et on devrait donc avoir l’impression que ça marche tout le temps.

Quand j’avais rencontré ce problème, j’avais demandé à un collègue s’il connaissait une solution pour réaliser cette opération, et du fond de sa mémoire bien remplie mais n’ayant pas fait d’électronique analogique depuis un moment, il m’a sorti ce schéma :

impulsion-idee

L’idée était là, il fallait maintenant trouver le schéma exact et les bonnes valeurs de composants. Quelques recherches m’ont amené sur ce Cours de mécanique et électromagnétisme écrit par Jean-Louis Cayrel de l’IUT de Saint-Étienne. Dans la section 1.11.6  Utilisation des condensateurs, il y a un point appelé Transformation Front → Impulsion qui explique très bien le principe du montage. Il s’agit d’un filtre passe-haut : la capacité laisse d’abord passer le front montant, cette transition brutale étant une haute fréquence, puis se charge à travers la résistance jusqu’à bloquer le passage du courant quand il n’y a plus de variation du signal, coupant ainsi les basses fréquences. On obtient une impulsion ! Lorsque le signal repasse à 0, un phénomène similaire se produit mais l’impulsion est cette fois négative. La diode sert à presque supprimer les impulsions négatives puisqu’elle va limiter les tensions négatives à sa tension de seuil.

La charge de la capacité doit être rapide pour obtenir une impulsion qui ressemble vraiment à une impulsion et pas à une sorte de gros triangle. La durée de charge est dépend du très classique τ = R * C, la constante de temps d’un circuit RC. En effet, la fréquence de coupure du filtre est fc = 1 / τ = 1 / (R * C). Ainsi, plus τ sera petit, plus la fréquence de coupure sera grande et plus le filtre « coupera rapidement » le créneau.

Pour vérifier tout ça, j’ai bien sûr commencé par simuler mon schéma avec LTSPiceIV ! Voici le schéma :

impulsion-schema

J’ai fait le montage « en double », avec d’un côté des valeurs fixes et de l’autres côté une valeur fixe pour R mais variable pour C. Le seul intérêt de la partie de gauche est de pouvoir tracer Vout1 d’une couleur distincte de Vout2 et ainsi marquer clairement la courbe de référence, celle avec la valeur la plus faible de  τ, c’est-à-dire le filtre le plus « agressif ».

Le résultat de la simulation est le suivant :

impulsion-simulation

Je n’ai plus eu qu’à choisir les valeurs pour mon montage et tester ça sur plaquette d’essai. Pas de surprise : ça fonctionnait parfaitement ! J’ai donc mis un tel filtre entre mes blocs secondes et minutes entre mes blocs minutes et heures de mon horloge Nixie et mes boutons fonctionnaient nickel !

 

Overhead d’une classe et de son constructeur C++ sur une structure C

Il y a quelques temps, il y a eu une discussion assez animée sur Developpez.com autour des avantages de C++ par rapport à C. Comme trop souvent, des gens ont exposés des croyances et des convictions sans fondement ou a minima pas complètement vérifiées. Je débute dans mon utilisation professionnelle du C++, je développe sur micro-contrôleurs relativement contraints et surtout j’aime bien les preuves. Quand il a été dit que les constructeurs C++ étaient très coûteux et nécessitaient des mécanismes complexes, j’ai tiqué : sans avoir vérifié, je ne voyais rien qui pouvait justifier cela a priori. Un constructeur n’est souvent qu’une fonction qui initialise des variables…

Je suis donc allé sur Compiler Explorer (1), un site fort sympathique où vous écrivez du code source à gauche et où le code assembleur correspondant est généré à la volée à droite. Vous pouvez choisir parmi un nombre important de compilateurs dans différentes versions, changer les options de compilation et surtout, des blocs de couleurs font la correspondance entre les lignes du code source et les instructions assembleur correspondantes.

Puisque je travaille sur des interfaces graphiques, j’ai choisi de modéliser un écran avec seulement 2 propriétés : hauteur et largeur. J’ai donc écrit plusieurs codes C et C++ et comparé les codes assembleur générés. Je vous partage les captures d’écran faites depuis Compiler Explorer, vous pouvez cliquez dessus pour les ouvrir en taille réelle.

Pour commencer, voici une classe C++ très basique et très classique :

1-cpp-classe-et-constructeur

On voit tout de suite qu’un constructeur ne génère pas des tonnes d’assembleur et n’utilise pas de mécanique complexe. Comparons tout de suite cela à une structure C avec une fonction d’initialisation :

2-c-struct-et-fonction-creation

C’est donc confirmé : non, un constructeur C++ ne coûte pas les yeux de la tête comparé à une fonction C. Ici, il y a un peu plus d’assembleur mais ça ne change pas la face du monde (2).

Le hic est que ce code C n’est pas très idiomatique : on n’a pas besoin d’une fonction pour créer et initialiser une structure en C…. Voici donc un second code avec la même structure initialisée de manière classique, ainsi qu’une nouvelle fonction d’affichage :

3-c-idiomatic

L’initialisation de la structure tient maintenant en 2 instructions assembleur, c’est beaucoup mieux. Remarquez au passage qu’un appel de fonction (même vide), ça coûte ! Il faut maintenant trouver une alternative crédible en C++. On ne va se trainer un constructeur C++ si coûteux comparé à une bête liste entre accolades en C ! La première idée est celle d’une classe template. On a généralement un seul écran donc la duplication de code liée à la spécialisation des templates n’est pas un problème et la classe sera optimisée à la compilation pour les dimensions de l’écran (bon OK, ici, la classe est vide…). Voici ma classe template et son code assembleur :

4-cpp-template

Le code assembleur est le même que celui de la version C ! WTF ?! Un template C++ ça ne coûte pas super cher ? Et ben non : un template n’est rien de plus qu’une sorte de super macro. Quand on l’instancie, le contenu est copié et les paramètres formels sont remplacés par les arguments de l’instanciation.

Il y a une autre alternative en C++ dans le cas où la duplication du code n’est pas souhaitable : l’aggregate initialization. Pour que cela soit possible, il faut que la classe respecte certaines contraintes, notamment celui que ces champs soient publiques. On a alors un code source comme celui-ci, pour le même code assembleur généré :

5-cpp-aggregate-type

Pour conclure, on voit qu’une classe C++ simple n’est pas vraiment plus coûteux qu’une simple structure en C. Il y a des mécaniques C++ coûteuses, comme les fonctions virtuelles, les exceptions, mais vous n’êtes pas obligés de les utiliser si vous avez de vraies contraintes de tailles et de performances. On peut faire du code en C++ qui a des performances et une empreinte mémoire semblable à un code C, mais le C++ vous apporte des choses intéressantes que vous n’aurez pas en C. Le sujet de cet article n’est pas de lister ces choses intéressantes mais bien de montrer qu’il ne faut pas avoir d’a priori, dans un sens comme dans l’autre, et qu’il est nécessaire de tester, de se documenter et de vérifier.

(1) J’ai découvert ce site en regardant la vidéo de la conférence de Jason Turner à la CppCon 2016. Il montre comment il a codé le jeu pong sur Commodore64 en en C++17. Il utilise les fonctionnalités les plus avancées du langage et montre l’assembleur au fur et à mesure. Cette vidéo est incroyable et détruit le mythe qui voudrait que le C++ moderne et évolué ne permet pas de produire du code compact ! À regarder absolument !

(2) Vous remarquerez que les codes ci-dessus ont été compilés en -O0, c’est-à-dire sans optimisation. Dés que les optimisations sont activées, à partir de -O1, tous ces codes produisent (naturellement ?) l’assembleur suivant :

main:
mov eax, 560
ret

Horloge Nixie !

En fait, mon horloge Nixie, sujet de tant d’articles sur ce blog depuis un plus d’un an, est terminée depuis un moment ! J’ai juste que je n’ai pas posté de l’été alors je n’ai pas eu l’occasion de vous la montrer. Enfin…. seule la partie électronique est terminée : il faut encore lui faire une boite.Voici quelques photos !

Étape 1 : réalisation des PCB

J’ai déjà expliqué comment faire sa carte maison, mais contrairement à la fois précédente, la gravure a été faite au perchlorure de fer. En effet, la carte principale était trop grande et on n’avait pas de bain-marie assez grand pour le faire avec le joli acide bleu. La petite carte a en profité pour faire trempette dans le même bain. Un grand bravo à mon ami Brice pour la mise en place super précise des calques double-face : du beau travail !

nixie_1nixie_2nixie_3

Étape 2 : réunir tous les composants

Je vous passe l’étape fastidieuse du perçage des dizaines de petits trous… Un travail hypnotique largement réalisé au falab de Nantes, le Platforme C.

La plupart des composants sont très simples à trouver et, comme d’habitude, je suis allé chez e44 à Nantes. Les tubes Nixie avaient été achetés il y a très longtemps sur E-Bay et vous pouvez encore en trouver.

nixie_4nixie_5nixie_6nixie_7

Étape 3 : soudage

Ah ! Les douces vapeurs d’étain 🙂 Heureusement, c’était en juin, c’était l’été, on pouvait ouvrir la fenêtre pour ventiler.

Comme les étapes précédente, c’était long, la carte est grande. Ah oui : j’ai fait la carte générant le signal 1 Hz en même temps mais elle a été rapidement expédié et j’ai oublié de la photographier.

La seule vraie difficulté était les tubes… Déjà parce qu’ils sont compliqués à insérer, les pattes sont nombreuses, c’est galère de toutes les faire rentrer en même temps dans le bon ordre. Heureusement, j’avais retenu une technique de fouine lue il y a très longtemps sur le net : couper les pattes pour faire une sorte d’escalier. Très efficace mais attention à ne pas couper trop court ! Il faut prévoir l’épaisseurs de la carte et du porte-tube ! Ensuite, il a fallu ruser fallu ruser pour stabiliser le tout lors de la soudure. Dernier point, il y a une piste sur la face composant qui arrive sous chaque tube… Sous le tube… Galère à souder. La technique est de mettre un peu d’étain, de faire chauffer et d’appuyer pour enfoncer le tube pendant que l’étain est encore liquide ! Ou sinon, faire un strap sous la carte pour « remplacer » la piste. Ça marche aussi et c’est plus simple ^^

nixie_8nixie_9nixie_10nixie_11nixie_12

Étape 4 : it’s alive!

J’avais confiance mais je suis d’un naturel pessimiste… J’ai connecté les cartes entre elles, j’ai branché au secteur… et la lumière fût ! Et avec elle une superbe horloge \o/

Tout ou presque a fonctionné directement. J’ai fait une modification sur la carte car le passage de 23:59 à 00:00 ne se faisait pas… On passait à 24:00, puis à 24:59 on revenait à 05:00. Une piste coupée, un fil et une diode soudés et c’était réparé. Il reste un défaut avec les boutons pour incrémenter les minutes et les herues. Si cela marchait très bien sur ma plaquette d’essai, ce n’est pas le cas ici. L’anti-rebond fonctionne bien mais le bouton devient inopérant pour quelques secondes. Sans doute que la capacité ne se décharge pas mais je n’ai pas encore eu la motivation de brancher l’oscilloscope et de regarder comment corriger ce défaut.

nixie_13nixie_14nixie_15

Conclusion

Ainsi s’achève ce long périple électronique ! Il est possible que je fasse encore un ou deux articles à propos de cet électronique mais je pense que c’est la fin de la série 🙂

Pour ceux que ça intéresse, les schematics et routages sont sur Github (à ouvrir avec Eagle). C’est sous licence open-hardware CERN donc faites vous plaisir !

Maintenant, la boite de l’horloge et trouver de nouveaux projets pour geeker à la maison !