C++

std::optional du C++17

C++17 a apporté son lot de nouvelles fonctionnalités et l’une est l’ajout de std::optional<class T>. Comme son nom l’indique très bien, c’est une classe template qui contient une valeur optionnelle. Pratique quand vous voulez écrire une fonction qui renvoie une valeur et en même temps signaler qu’il est possible que cette valeur soit invalide et ne doive pas être utilisée. Pour plus de détails sur cette fonctionnalité, vous pouvez bien sûr lire le très bon article de cppreference.

Pour l’exemple, j’ai fait écrit une fonction qui tente de charger une image. Comme le chargement peut échouer, elle ne renvoie pas directement une instance de la classe Image mais un std::optional<Image> pour wrapper une image qui peut ne pas être présente. Voici le code :

Rien de bien sorcier. Une classe Image avec ici juste le chemin vers l’image. La fonction load_image() regarde si le chemin qu’on lui fournit correspond à une image connue et renvoie alors une image ; sinon, elle renvoie std::nullopt, une constante magique pour créer un objet optionnel absent. La fonction try_to_load() montre comment on peut ensuite se servir d’un std::optional. Un tel objet possède un opérateur de conversion en booléen pour tester s’il contient une valeur ou pas. Alternativement, vous pouvez utilisez sa méthode has_value(), qui a une sémantique plus explicite. Une fois le test fait, la méthode value() permet de récupérer l’objet wrappé, ici une instance d’Image qui a donc une méthode getPath().

Petit détail important, l’objet wrappé est contenu dans le std::optional. Ce dernier ne contient pas un pointeur vers la valeur et aucune allocation dynamique n’est effectuée ici.

Personnellement, je trouve ça sympa 🙂

Publicités

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.


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 !


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