Nouveau

C vs C++ : initialisation des variables statiques

On lit souvent C/C++. Hé hé… S’il est indéniable que le ++ de C++ n’est pas là pour rien, il faut aussi savoir qu’il y a des différences en forme de pièges dans la partie « commune ». Ces différences sont particulièrement trompeuses quand on écrit du code C++ qu’on souhaite être compatible C. On va s’intéresser aujourd’hui à l’initialisation des variables statiques.

Voici un code C++ a priori anodin :

#include <stdlib.h>

int getValue()
{
    return rand();
}

int f(int c)
{
    static const int initial = getValue();

    if (initial != c)
        return c;
    else
        return initial;
}

int main()
{
    return f(42);
}

La ligne intéressante est la ligne 10 : on initialise une variable statique avec la valeur de retour d’une fonction.

Plaçons ce code dans un fichier main.cpp, plaçons ce fichier dans un projet Eclipse, et laçons un build :

[pre]16:52:21 **** Incremental Build of configuration Debug for project Experiences ****
Info: Internal Builder is used for build
g++ -std=c++0x -O0 -g3 -pedantic -Wall -Wextra
-c -o « src\\main.o » « ..\\src\\main.cpp »
g++ -o Experiences.exe « src\\main.o »

16:52:21 Build Finished (took 498ms)[/pre]

Renommons simplement le fichier pour forcer sa compilation en C. En effet, GCC se base sur l’extension du fichier pour savoir comment le compiler. Voici le résultat du build dans ce cas :

[pre]16:53:12 **** Incremental Build of configuration Debug for project Experiences ****
Info: Internal Builder is used for build
gcc -std=c99 -O0 -g3 -pedantic -Wall -Wextra
-c -o « src\\main.o » « ..\\src\\main.c »
..\src\main.c: In function ‘f’:
..\src\main.c:10:32: error: initializer element is not constant
static const int initial = getValue();
^

16:53:12 Build Finished (took 139ms)
[/pre]

L’erreur est claire. On pourrait soupçonner le qualificatif const mais cela ne change rien si on l’enlève. Le résultat est le même si la variable est statique au fichier au lieu d’être statique à la fonction.

Sans aller creuser dans les spécifications des langages, on se doute que C résoud la valeur d’une variable statique à la compilation alors que C++ autorise qu’elle soit résolue à l’exécution. Pour cela, le compilateur va sans doute générer un peu de code pour savoir si la variable a déjà été initialisée et l’initialiser le cas échéant (inutile de préciser que cela a un coût). On peut le vérifier en regardant le code assembleur de la fonction f() :

[pre] f(int):
0040144d: push %ebp
0040144e: mov %esp,%ebp
00401450: sub $0x18,%esp
10 static const int initial = getValue();
00401453: mov $0x40d020,%eax
00401458: movzbl (%eax),%eax
0040145b: test %al,%al
0040145d: jne 0x40148a
0040145f: movl $0x40d020,(%esp)
00401466: call 0x4014c4
0040146b: test %eax,%eax
0040146d: setne %al
00401470: test %al,%al
00401472: je 0x40148a
00401474: call 0x401440
00401479: mov %eax,0x40d028
0040147e: movl $0x40d020,(%esp)
00401485: call 0x4014bc
12 if (initial != c)
0040148a: mov 0x40d028,%eax
0040148f: cmp 0x8(%ebp),%eax
00401492: je 0x401499
13 return c;
00401494: mov 0x8(%ebp),%eax
00401497: jmp 0x40149e
15 return initial;
00401499: mov 0x40d028,%eax
16 }
[/pre]

Changeons l’initialisation de notre variable par quelque chose de constant, par exemple :

static const int initial = 666;

Voici le nouveau code assembleur, beaucoup plus simple, de la même fonction f() :
[pre] f(int):
0040144d: push %ebp
0040144e: mov %esp,%ebp
12 if (initial != c)
00401450: cmpl $0x29a,0x8(%ebp)
00401457: je 0x40145e
13 return c;
00401459: mov 0x8(%ebp),%eax
0040145c: jmp 0x401463
15 return initial;
0040145e: mov $0x29a,%eax
16 }
[/pre]

Pour avoir un code similaire en C, il faudrait implémenter une protection similaire, comme ceci :

#include <stdbool.h>
int f(int c)
{
    static int initial;
    static bool guard = false;

    if (guard == false)
    {
        guard = true;
        initial = rand();
    }

    if (initial != c)
        return c;
    else
        return initial;
}

On voit quelques inconvénients : perte de lisibilité, perte du qualificatif const, aucune garantie de thread-safety. A noter qu’en C++, le caractère thread-safe d’un tel code semble dépendre de la version du langage, comme débattu dans cette discussion stackoverflow « Is local static variable initialization thread-safe in C++11?« .

C’est tout pour aujourd’hui, vous êtes prévenus 🙂

Édition du 05/04/2017 : il s’avère en fait que la nécessité d’un initializer constant en C ne se limite pas aux variables déclarés avec le mot-clé static. Cette contrainte s’applique à toutes les variables ayant une static storage duration, comme expliqué dans cette discussion stackoverflow « Error “initializer element is not constant” when trying to initialize variable with const« . Ainsi, cette contrainte s’applique aussi aux variables globales.

Publicités

XBee et XCTU sous Linux

Cela doit bien faire 2 ans que deux modules XBee (des Pro S1 de chez Digi) traînent dans mes tiroirs et je n’en avais rien fait. Ayant pour projet de faire des capteurs de température et d’humidité sans fil, je me suis dit que c’était le bon moment de les utiliser. L’idée est d’envoyer les données à un Raspberry Pi pour traitement. J’ai donc pris mon XBee Explrer Dongle, j’y ai connecté un module Xbee et je l’ai branché à mon PC. Digi fournit un logiciel pour gérer les modules XBee connectés à un PC, les configurer, envoyer et recevoir des données : il s’agit de XCTU. De nombreux tutoriels sur Internet disent que XCTU n’est disponible que sous Windows mais cette époque est révolue ! Vous pouvez télécharger les différentes versions pour Windows, Linux et Mac ici.

Installation et lancement

Si vous êtes sous Linux et que vous ne savez pas si vous devez opter pour la version 32 ou la version 64 bits, il vous suffit de taper la commande uname -a pour savoir si vous avez un processeur 32 ou 64 bits. Exemple dans mon cas :

~$ uname -a
Linux pgradot-xubuntu 4.4.0-53-generic #74-Ubuntu SMP 
Fri Dec 2 15:58:04 UTC 2016 i686 i686 i686 GNU/Linux

Le i686 m’indique que mon processeur est 32 bits.

Une fois le téléchargement terminé, rendez le fichier exécutable et lancez-le avec les droits administrateurs :

$ chmod u+x 40002880_G.run
$ sudo ./40002880_G.run

Un wizard vous guide dans l’installation de XCTU. Par défaut, le logiciel s’installe dans /opt/Digi où un sous-dossier XCTU-NG est créé. On y trouve launcher et app (le premier lançant le second). Si vous lancez l’un des deux sans les droits administrateurs, cela ne fera qu’ouvrir une petite fenêtre qui ne contient rien et que vous ne pourrez pas fermer… Ayez le réflexe sudo, ça marchera beaucoup mieux ! Vous verrez alors ceci :

xbee 1 startup

Ajout de modules

Vous pouvez ajouter des modules radios ou les découvrir en cliquant que les icônes en haut à gauche. L’ info-bulle vous avait sans doute déjà fait deviner le mode opératoire. Le mien étant branché via un dongle USB, il apparaît comme étant /dev/ttyUSB0 :

xbee 2 add device

XCTU récupère et affiche tout la configuration du nouveau module :

xbee 3 device added

Onglets configuration, console et réseau

XCTU est intuitif et l’info-bulle de droite nous dit qu’il existe 3 onglets, chacun correspondant à un mode :

  1. Le mode configuration, ouvert par défaut, permet de voir la configuration des modules comme montré précédemment mais aussi de la modifier et d’envoyer la nouvelle configuration au module.
  2. Le monde console permet soit d’interagir via des commandes AT avec un module, soit d’envoyer des paquets sur le réseau. Vous pouvez enregistrer des paquets et jouer des séquences d’envoi. Certainement très pratique pour des tests.
  3. Le mode réseau, pour avoir un aperçu de la topologie du réseau sans fil à portée.

Commandes AT

Si vous souhaitez envoyez des commandes AT à votre module, il suffit d’aller dans l’onglet console et d’appuyer sur le bouton Open (qui se transforme en bouton Close, visible en vert dans l’image ci-dessous). Vous tapez alors +++ dans à gauche, vous attendez une seconde et le module répond OK pour indiquer qu’il est bien passé en mode commande. Tapez alors vos commandes en les terminant par entrée et le module vous répondra. Sans activité de votre part pendant 3 secondes, le module sort automatiquement et silencieusement du mode commande et l’envoi d’une nouvelle commande sera alors sans réponse. Sparkun a écrit un très bon aide-mémoire pour les commandes AT disponibles.

xbee 4 at

Pour aller plus loin

Exploring XBees and XCTU par Sparkun

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 !