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#.

Publicités

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s