Version en ligne

Tutoriel : Débugger avec Qt

Table des matières

Débugger avec Qt
Comment débugger ?
Mode release/debug
Afficher des messages de débuggage
Rediriger les flux
Un code unique quel que soit le mode de compilation
Fonctions de débuggage

Débugger avec Qt

Comment débugger ?

Lorsque l'on programme, il y a une constante qui revient à chaque fois : le débuggage.

Petit rappel pour ce qui ne savent pas ou plus : le débuggage est « l'art » de suivre toutes les opérations d'un programme afin de déterminer l'origine d'une ou plusieurs erreurs.

En effet, lorsque l'on réalise un programme ou une librairie (quel que soit le langage) il y a toujours un moment où on a besoin de débugger. Il s'agit en général d'un travail long et souvent pénible car les erreurs n'apparaissent pas toujours lors de la compilation. Il faut donc faire en sorte de les traquer une à une.

Ce tutoriel vous propose donc d'apprendre une des nombreuses manières de débugger vos programmes ou vos librairies, et ce grâce à Qt.

Comment débugger ?

Mode release/debug

Il existe de nombreuses manières de débugger, certaines fonctionnant mieux que d'autres. Mais, en général, toutes fonctionnent relativement bien et ont chacune leurs avantages / désavantages.
Le choix d'une technique de débuggage ressemble au choix d'un OS (Operating System : système d'exploitation) : chacun dit que le sien est meilleur que les autres et tente d'en convaincre tout le monde (j'exagère un peu, je sais ;) ). Personnellement je pense que la meilleure technique de débuggage est celle avec laquelle vous vous sentez le plus à l'aise.

Parmi les nombreuses manières de débugger un programme, voici les deux principales :

Heu… Je ne vois pas trop où est la difficulté de choisir telle ou telle technique. Tu es sûr de ce que tu dis ?

Absolument certain, mais le débuggage ne s'utilise pas que pour débugger des programmes. On peut aussi débugger des librairies/plugins et autres objets non exécutables. Dans ces cas-là, il est impossible d'utiliser la visualisation des variables et les "breakpoints" directement sur les fichiers de sortie. En effet, ceux-ci ne disposent pas de fonction main, donc le débugger ne sait pas par où il doit commencer. Donc les debuggers perdent tout leur intérêt dans ce genre de cas. La seule manière d'utiliser un debugger avec une librairie/plugin, c'est de créer un code exemple puis de lancer le debugger avec l'exemple.
On peut donc deviner qu'il devient plus facile d'utiliser la deuxième méthode.

Comme je l'ai dit plus haut, ce tutoriel est là pour vous apprendre à vous servir des outils mis en place par Qt pour le débuggage. Comme Qt est une librairie, elle ne propose pas de debugger en tant que programme, mais elle offre de nombreux outils pour afficher des « choses » dans la console. C'est donc sur cet aspect qu'on va se pencher.

De plus, les fonctions de débuggage proposées par Qt peuvent transmettre des informations au debugger (principalement sous Windows). Donc la suite n'est pas forcément à négliger.


Mode release/debug

Mode release/debug

Comment débugger ? Afficher des messages de débuggage

La première chose à savoir lorsqu'on souhaite faire du débuggage avec Qt, c'est qu'il existe, en gros, deux modes de compilation :

D'une manière générale, lorsque vous téléchargez une librairie, vous obtenez la version release (s'il y avait un mode debug). En effet, une librairie en mode debug ne donne des informations que sur elle-même en cas de problème interne, donc ça importe peu pour la plupart des utilisateurs. Cependant, il reste toujours possible de récupérer la version debug d'une librairie.
Comme la librairie Qt se veut « libre », et qu'on peut la recompiler à volonté, les créateurs ont aussi donné accès aux versions debug, celles-ci étant incluses dans les « packages ».

Changer le mode

Comme vous pouvez le constater, il peut être fort utile de pouvoir compiler à volonté soit en mode debug, soit en mode release. D'ailleurs, le changement se fait non seulement pour les librairies de Qt dont vous avez besoin, mais cela permet deux-trois choses dans votre propre programme ou votre propre librairie que nous verrons légèrement plus tard.

Pour changer de mode, c'est très simple. Si vous avez suivi le tutoriel de M@teo21 sur Qt, tâchez de vous rappeler comment on compile un projet. En fait, ça se résumait à trois « instructions » dans la console (dans le cas d'un nouveau projet). Les voici :

qmake -project
qmake
make

Si on prend les instructions une à une, voici ce qu'elles font :

La modification que nous devons apporter se situe entre la première et la deuxième étape. En effet, le mode de compilation se décide dans le fichier de projet.
Si vous l'ouvrez (avec le bloc-notes par exemple), vous devriez obtenir quelque chose comme ceci :

######################################################################
# Automatically generated by qmake (2.01a) lun. 23. mars 22:46:38 2009
######################################################################

TEMPLATE = app
TARGET = 
DEPENDPATH += .
INCLUDEPATH += .

# Input
SOURCES += main.cpp

Pour le réglage du mode de compilation, tout se passe avec la variable CONFIG. Comme celle-ci est déjà pré-remplie (même si on ne la voit pas), il faudra employer le symbole += pour indiquer qu'on souhaite ajouter des propriétés.

Si vous souhaitez faire de la compilation en mode debug, vous avez deux choix :

Et si vous préférez le mode release, il suffit de mettre « release » à la place de « debug ».

Facile, non ?

Quel que soit le mode de compilation choisi, vous pouvez à tout moment ajouter aussi « console ». Cela force — dans le cas d'une application graphique — à ouvrir une console en arrière-plan. C'est très pratique pour voir vos propres messages de débuggage (on va voir dans un instant comment faire) mais aussi pour voir ceux de Qt, notamment concernant les signaux ou les slots qui ne se sont pas connectés ensemble (quelle qu'en soit la raison).

Imaginons qu'on se place en mode debug. Voici ce qu'on obtient :

######################################################################
# Automatically generated by qmake (2.01a) lun. 23. mars 22:46:38 2009
######################################################################

TEMPLATE = app
CONFIG +=  console debug
TARGET = 
DEPENDPATH += .
INCLUDEPATH += .

# Input
SOURCES += main.cpp

Vous pouvez voir que j'ai aussi ajouté le mot « console ».

Mode debug et release en même temps

En fait, il est possible de considérer qu'il existe un troisième mode de compilation… Celui-ci est une combinaison des deux autres car il effectue les deux compilations en même temps (enfin l'une après l'autre, sans pause).
Pour cela, remplacez le « debug » ou le « release » par « debug_and_release ».

Dans ce cas, pour compiler, il vous faudra faire make all à la place de make.
Cependant, il reste possible d'utiliser make pour compiler les deux modes. Pour faire ça, ajoutez en plus de « debug_and_release » un « build_all ». Voilà, maintenant vous pouvez compiler les deux modes à la suite avec un simple make.

Prêt(e) à entrer dans le débuggage profond ?


Comment débugger ? Afficher des messages de débuggage

Afficher des messages de débuggage

Mode release/debug Rediriger les flux

Enfin, on attaque la partie vraiment pratique.

Avant que tu ne commences… Pourquoi présenter des fonctions faites par Qt alors qu'un simple « cout » suffirait (maintenant qu'on sait afficher la console) ?

Très bonne question. En fait absolument rien ne vous empêche d'employer le bon vieux (mais fidèle) « cout ».
Mais voici deux-trois raisons de ne PAS l'utiliser :

Après c'est à vous de voir ce que vous préférez.

Les fonctions de base

Qt intègre quatre fonctions dédiées au débuggage et aux rapports d'erreurs. Chacune a un but défini. Les voici :

Je vous invite vivement à consulter la doc les concernant, c'est très utile.
Par défaut, les messages sont envoyés sur « stderr » (le flux standard des erreurs). Dans le cas d'une application graphique sans console, c'est envoyé vers le débugger (s'il est présent, sinon adieu le message :-° )

ATTENTION : Ces fonctions sont très très basiques et fonctionnent comme les (très vieilles) fonctions du C, c'est-à-dire du genre de la fonction « printf » (ouh, que c'est vieux). C'est-à-dire une chaîne de caractère avec des « %s » pour les chaînes de caractères, « %d » pour les entiers, etc.
De plus elles vont automatiquement à la ligne. Donc pas besoin de finir avec des «  ».

QUOI ? Mais mais… et la classe QString ?

Je pense que les programmeurs de Qt (merci à Nanoc), lorsqu'ils ont créé ces fonctions, ont réfléchi à la chose suivante : si on emploie des fonctions comme qCritical ou qFatal, c'est qu'a priori quelque chose ne va pas. Si on utilise la classe QString alors que quelque chose ne va pas, QString risque aussi d'avoir un problème. Et là on va avoir de gros problèmes car une erreur se produit pendant le rapport d'une autre erreur. Et cette erreur va elle aussi être rapportée en appelant QString, qui provoquera une nouvelle erreur qui…

Assez peu pratique, non ? Et puis dans le cas où il se produit une erreur dans QString, on ne peut pas appeler les fonction prévues car celle-ci fonctionne avec QString.

Cependant, dans le cadre de vos projets, vous partez de l'hypothèse que Qt fonctionne correctement. De toute manière, s'il y a un problème interne, c'est Qt qui gère. Donc rien ne vous empêche d'utiliser QString avec ces fonctions. Et comme nous parlons de Qt, ils ont quand même prévu les choses, et mettent donc à notre disposition deux mécanismes pour nous simplifier la vie.

Simplifiez-vous la vie

qPrintable

Certains connaissent déjà cette fonction miracle : qPrintable. On en peut pas faire plus simple : vous lui donnez en paramètre une QString et elle vous sort... un « char * ». Parfait pour le donner à votre fonction.

Voyons un exemple :

qFatal(qPrintable(tr("Erreur fatale : la variable %1 n'existe pas").arg(i)));

Comme vous pouvez le voir, ça permet aussi d'utiliser directement la fonction « tr » (pour d'éventuelles traductions !).

Personnellement, je vous conseille de créer une petite macro, du genre :

#define FATAL_ERROR(msg)    qFatal(qPrintable(msg))

Comme ça, plus de problème.

Passons donc au deuxième mécanisme (le meilleur).

QtDebug

Comme je vous l'ai dit légèrement plus haut, un des avantages de Qt c'est que nombre de ses classes savent se représenter seules dans une console. Pourtant aucune ne semble avoir de fonction du style char * debug () const. Alors comment font-elles ?

En fait il est possible d'utiliser nos quatre fonctions comme « cout », c'est-à-dire avec des opérateurs de flux. Pour cela, il suffit d'inclure « QtDebug » dans votre fichier :

#include <QtDebug>

Rien ne vaut un bon exemple, donc :

qDebug() << "Début du programme, il est " << QTime::currentTime().toString("HH:mm::ss");

Notez la présence des parenthèses après l'appel de la fonction. Elles sont obligatoires. Comme leur version « de base », ces fonctions vont automatiquement à la ligne.

Les fonctions supra-complexes

Comme chacun le sait peut-être, Qt ne fait pas dans les demi-mesures. Donc on dispose encore de deux fonctions… très particulières. Celles-ci font partie intégrante de la classe QObject, donc tout le monde (ou presque) en dispose :

À vous de tester !

Avec ça, on a déjà de quoi réfléchir, non ? Alors pour ceux qui ont réussi à digérer, voici la suite.


Mode release/debug Rediriger les flux

Rediriger les flux

Afficher des messages de débuggage Un code unique quel que soit le mode de compilation

Comme je l'ai dit, les messages sont expédiés sur stderr. De ce fait, dans le cas d'une application graphique, on est obligé de faire afficher la console. Ce n'est pas très beau (surtout lorsqu'elle disparaît dès la fin du programme).

Le truc génial, ça serait de pouvoir expédier le tout dans un fichier. Mais à faire pour les quatre fonctions ça risque de se révéler pénible. Heureusement, grâce à Qt, on peut rediriger chaque fonction vers le même fichier ou chacun dans le sien. On peut même inserer du texte « obligatoire » (comme « WARNING : » pour chaque appel à qWarning).

La méthode n'est peut-être pas des plus claires, mais elle marche. Pour commencer, il faut déclarer une fonction ayant ce prototype (rappel : seul le nom de la fonction peut changer) :

void myMessageOutput (QtMsgType type, const char *msg)

Ensuite, c'est dans cette fonction que vous déciderez de faire ce que vous voulez des messages, etc.

Pour mettre en application cette fonction, il suffit de faire :

qInstallMsgHandler(myMessageOutput);

De préférence dans le main, avec QApplication.

Le mieux est de vous mettre l'exemple de Qt (visible ici) :

#include <qapplication.h>
 #include <stdio.h>
 #include <stdlib.h>

 void myMessageOutput(QtMsgType type, const char *msg)
 {
     switch (type) {
     case QtDebugMsg:
         fprintf(stderr, "Debug: %s\n", msg);
         break;
     case QtWarningMsg:
         fprintf(stderr, "Warning: %s\n", msg);
         break;
     case QtCriticalMsg:
         fprintf(stderr, "Critical: %s\n", msg);
         break;
     case QtFatalMsg:
         fprintf(stderr, "Fatal: %s\n", msg);
         abort();
     }
 }

 int main(int argc, char **argv)
 {
     qInstallMsgHandler(myMessageOutput);
     QApplication app(argc, argv);
     ...
     return app.exec();
 }

Voilà, vous savez comment détourner les messages d'erreur pour les expédier où vous voulez (même dans le néant :-° ).
Dans la prochaine partie, je vais vous donner encore deux conseils pour mieux débugger, puis je vous laisse vous débrouiller.


Afficher des messages de débuggage Un code unique quel que soit le mode de compilation

Un code unique quel que soit le mode de compilation

Rediriger les flux Fonctions de débuggage

Tout au long de ce tutoriel, il y a quelque chose dont je n'ai pas parlé, mais que j'aimerais quand même vous présenter car vous allez forcément tomber dessus un jour.

Le problème

Imaginez que vous avez fait un superbe programme, et en vous souvenant de ce tutoriel, vous l'avez truffé de qDebug et qWarning.
Maintenant que vous l'avez débuggé à 100 %, vous voulez faire le compiler en mode release. Rien de plus facile, vous changez votre projet et hop, c'est fait.

Et où est le problème ?

Maintenant exécutez le programme. Et, oh, stupeur, les messages s'affichent aussi. Pas très joli pour la première version de votre super programme.
Bien évidemment, vous pouvez les enlever puis recompiler. Mais il va falloir le faire à chaque fois... Un peu pénible, avouez-le.
Pour pallier ce problème, il existe deux solutions.

Les solutions

La première consiste à modifier votre projet en ajoutant à DEFINES (comme pour CONFIG) ceci : QT_NO_DEBUG_OUTPUT et QT_NO_WARNING_OUTPUT. Ça va enlever les messages, mais ça ne marche que pour qDebug et QWarning.

La deuxième provient d'une simple observation : lorsque vous compilez en mode release, le « #define » QT_NO_DEBUG est passé à l'ensemble de vos fichiers.
Si on résume, vous disposez d'un « #define » automatique dans le cas d'une compilation en mode release. Dans les autres cas, il n'est pas là. Il faut donc, dans votre code, faire ceci pour chaque message :

#ifndef QT_NO_DEBUG
    qDebug("...");
#endif

Dans la mesure où ça peut être très fastidieux, je vous propose (vous faites comme vous l'entendez) de procéder comme suit, dans un fichier .h inclus partout :

#ifdef QT_NO_DEBUG
    #define debug(msg)
#else
    #define debug(msg)    qDebug(qPrintable(msg));
#endif

Analysons le code rapidement : dans le cas où vous êtes en mode debug, tout va bien, la fonction « debug » est remplacée par qDebug. Lors de l'exécution, les messages s'affichent correctement.

Mais si vous passez en mode release, lorsque vous appelez la fonction « debug », elle est remplacée par… rien ! Bien évidemment les messages ne s'affichent plus lors de l'exécution (et pour cause !).

De cette manière donc, vous pouvez utiliser la fonction « debug » directement avec les QString et n'afficher les messages que si vous êtes en mode debug.


Rediriger les flux Fonctions de débuggage

Fonctions de débuggage

Un code unique quel que soit le mode de compilation

J'avais promis d'en finir, mais je ne peux m'empêcher de vous parler d'elles…

Même si la documentation est limpide en ce qui les concerne, nous sommes sur le Site du Zéro, donc je vais légèrement parler d'elles.

Elles sont là pour faire de simples tests et vérifier que tout va bien (elles sont souvent employées pour l'opérateur [] pour tester si l'index est dans les limites autorisées). Je vous demande donc d'applaudir d'accueillir Q_ASSERT et Q_ASSERT_X !

Q_ASSERT

Très basique. Vous lui donnez comme seul paramètre un test. Si le test est vrai, tout va bien, le programme se poursuit. Par contre, s'il a le malheur d'être faux… il est mal, très mal !

En effet, Qt ne plaisante pas avec Q_ASSERT. Si le test est faux, ça affiche automatiquement un petit message et pfiout… terminé le programme (d'ailleurs, il arrive à Windows de se plaindre de cette fin soudaine avec un deuxième message).

Le message possède une forme standard, il ressemble donc à ceci :

ASSERT: "Test" in file main.cpp, line 10

« Test » est bien sûr remplacé par votre test, ainsi que le nom du fichier et la ligne.

Bien que ce soit parfaitement compréhensible par un programmeur, le jour où un utilisateur lambda tombe dessus… Il est bien embêté.
C'est pourquoi nous passons à la deuxième candidate.

Q_ASSERT_X

Il s'agit en fait de la grande sœur de Q_ASSERT. En plus du test, elle accepte deux paramètres supplémentaires, qui peuvent facilement être décrits par et quoi :

Personnellement, j'ai plus tendance à employer la deuxième fonction, mais c'est vous qui décidez.

Rien ne parle mieux qu'un exemple, donc le voici :

#include <QCoreApplication>

int main (int argc, char **argv)
{
	QCoreApplication app(argc, argv);

	int * valeurs = new int[3];

	valeurs[0] = 0;
	valeurs[1] = 1;
	valeurs[2] = 2;

	int i = 0;
	while(i <= 3)
	{
		Q_ASSERT_X(i >= 0 && i <= 2, "main", "Valeur invalide pour 'i'");

		qDebug(qPrintable(QString("valeurs[%1] = %2").arg(i).arg(valeurs[i])));
		i++;
	}

	return app.exec();
}
valeurs[0] = 0
valeurs[1] = 1
valeurs[2] = 2
ASSERT failure in main: "Valeur invalide pour 'i'", file main.cpp, line 16

Bien évidemment, il s'agit d'un pur « cas d'école », le code ici présent ne servant pas à grand-chose.
Vous pouvez aussi voir que la fonction Q_ASSERT_X affiche toutes les informations nécessaires et quitte toute seule si le test échoue.

C'est fini ! FI-NI !

Vous voilà enfin déjà au bout de ce tutoriel. J'espère que mes explications sur le débuggage ont été comprises et surtout utiles.
Prenez bien le temps de relire, n'hésitez pas à repasser pour vérifier (j'ai moi-même été pris par de solides doutes durant l'écriture de ce tutoriel)

Maintenant je ne veux plus voir que des programmes qui écrivent deux pages A4 par fonction en mode debug.

Sur ce, je vous laisse retourner à vos chers programmes !


Un code unique quel que soit le mode de compilation