FAQ Qt
FAQ QtConsultez toutes les FAQ
Nombre d'auteurs : 26, nombre de questions : 298, dernière mise à jour : 15 juin 2021
- Un new sans delete ?
- Erreur d'édition des liens undefined reference to 'vtable for xxx' ?
- Allouer sur le tas ou sur la pile des QObject et dérivés ?
- Héritage multiple avec QObject ?
- Peut-on utiliser des pointeurs intelligents sans danger avec des QObject ?
- Comment faire un delete sur un QObject de manière sûre ?
- Un QObject en tant que membre d'une classe ?
Dans un code Qt, on verra souvent un new sans delete associé.
En fait, si une nouvelle instance de QObject est créée et qu'on lui spécifie un parent - le premier argument du constructeur -, c'est ce parent qui sera chargé de la destruction du fils.
La majorité des classes de Qt hérite plus ou moins directement de QObject, mais attention, ce n'est pas le cas de toutes. L'indicateur est la possibilité de passer un objet parent au constructeur.
Lien : Allouer sur le tas ou sur la pile des QObject et dérivés ?
Cette erreur se produit lorsque la partie QObject d'une classe n'a pas été ajoutée à l'édition des liens.
Lors de l'utilisation de la macro Q_OBJECT, on définit un certain nombre de méthodes et de variables statiques. Ces méthodes et ces variables sont implémentées dans un fichier généré automatiquement par qmake à l'aide de l'outil moc.
Pour corriger le problème, ajoutez la macro Q_OBJECT dans votre déclaration de classe puis relancez qmake. Si le problème subsiste, vérifiez que le dossier du fichier source est bien présent dans les INCLUDEPATH du fichier de projet pour que le moc passe bien sur les fichiers y étant présents.
Lien : La classe QObject
Lien : Les signaux et slots
Chaque QObject contient une liste des pointeurs vers les QObject fils. Cette liste permet d'effacer automatiquement les objets fils de cette liste.
Cette liste implique que les objets fils héritant de QObject devraient être alloués sur le tas et non la pile. A priori, le code d'effacement est fait de telle sorte qu'il ne devrait pas y avoir de problème en allouant un objet sur la pile car la destruction d'un objet entraîne sa suppression dans la liste des parents. En revanche, effacer manuellement ou automatiquement par destruction dans la pile les objets fils peut entraîner un surcoût. Enfin, étant utilisé avec une sémantique de pointeurs, il vaut mieux utiliser les pointeurs.
{
QObject
parent;
QObject
*
enfant =
new
QObject
(&
parent);
}
// enfant est correctement supprimé de la mémoire par parent
De même, une instance héritée de QObject ne peut appartenir à deux QObject.
QObject
*
parent =
new
QObject
;
QObject
*
enfant =
new
QObject
(parent);
QObject
autreParent;
enfant->
setParent(&
autreParent); // change le parent de enfant
delete
parent;
// enfant n'est pas supprimé, car parent ne "possède" plus enfant
La relation parent-enfant peut poser problème lorsque les QObject sont alloués sur la pile. Lorsqu'un objet est alloué sur la pile, et a un parent, le parent pourrait appeler delete sur l'objet qui est sur la pile.
{
// !!! exemple de ce qu'il NE faut PAS faire, ne pas recopier !
QPushButton
button("button"
);
QWidget
widget;
button.setParent(&
widget);
}
// plantage ici
// lorsque widget est détruit à la fin de la portée, il supprime button à l'aide de delete.
// button avait été alloué sur la pile
Lien : QObject
Lien : Le modèle QObject
L'héritage multiple de plusieurs QObject n'est pas possible. En effet, l'architecture de Qt - les métaobjets - rend la chose impossible à l'heure actuelle.
Lorsque vous faites un héritage multiple, mettez en premier la classe héritant de QObject.
QObject (et toutes les classes qui en dérivent) ne se marie pas très bien avec les pointeurs intelligents classiques du C++, tels que auto_ptr ou shared_ptr. Comme expliqué dans la question Un new sans delete ?, Qt implémente via QObject un mécanisme de gestion de la mémoire dont le principe est qu'un QObject parent détruise automatiquement tous ses objets QObject enfants.
Ce mécanisme est fort pratique et permet de grandement simplifier la gestion de la mémoire. Cependant, il entre en conflit avec tout autre mécanisme de libération de la mémoire tel que ceux implémentés au moyen de pointeurs intelligents.
En effet, si vous déclarez un pointeur intelligent sur une instance de QObject, ce dernier aura en charge la destruction de cette instance. Si par malheur votre QObject se trouve être l'enfant d'un autre QObject, et que ce parent vient à être détruit, votre instance sera elle aussi automatiquement détruite, alors que votre pointeur intelligent continuera de pointer vers elle ! Et quand votre pointeur intelligent estimera qu'il est temps de la détruire, il effectuera un appel à delete sur un objet déjà détruit (double utilisation de delete) avec toutes les conséquences fâcheuses que l'on connaît.
À noter que spécifier un parent nul au moment de la construction de votre QObject ne vous garantit pas pour autant que ce dernier ne sera pas par la suite reparenté via la fonction setParent(), chose qui se produit assez régulièrement entre objets graphiques (QWidget).
En conséquence, il est déconseillé d'utiliser des pointeurs intelligents sur des objets de type QObject. Si jamais vous devez néanmoins y avoir recours, vous devez protéger votre instance d'une double suppression au moyen d'un pointeur protégé (QPointer avec Qt 4), comme dans l'exemple suivant.
std::
shared_ptr<
QPointer
<
QWidget
>
>
ptr(new
QPointer
<
QWidget
>
>
( new
QWidget
() );
// Avec C++14, plus propre :
std::
shared_ptr<
QPointer
<
QWidget
>
>
ptr =
std::
make_shared<
QPointer
<
QWidget
>>
( new
QWidget
() );
On a deux niveaux avec cette construction std::shared_ptr< QPointer<QWidget> > :
- new QPointer<QWidget>(…). L'objet std::shared_ptr possède le pointeur vers QPointer<QWidget> : peu importe la situation du QWidget, il supprimera (delete) le pointeur vers le QPointer<QWidget> lorsque le dernier std::shared_ptr sera détruit ;
- new QWidget(). Le QPointer, lui, a un pointeur vers le QWidget et n'en a pas la garde. Que le QWidget soit déjà détruit ou non, il ne le supprimera pas. Seul son pointeur interne se mettra à nullptr si le QWidget est détruit.
Finalement, lorsque le (dernier) std::shared_ptr est détruit, si le QWidget ne l'est pas, il y aura une fuite de mémoire !
Vu la lourdeur de cette écriture et le changement de sémantique introduit, on comprend mieux pourquoi il est déconseillé d'employer des pointeurs intelligents sur des objets dérivant de QObject.
Si vous souhaitez néanmoins utiliser des pointeurs intelligents dans ce contexte, vous pouvez utiliser la classe suivante, qui règle automatiquement les problèmes de propriété. (Elle utilise des fonctionnalités disponibles uniquement à partir de C++11.)
#ifndef SCOPED_QPOINTER_HPP
#define SCOPED_QPOINTER_HPP
#include
<QPointer>
#include
<memory>
#include
<type_traits>
template
<
class
Object,
class
Deleter =
std::
default_delete<
Object>
>
class
ScopedQPointer {
public
:
static_assert
(std::
is_base_of<
QObject
, Object>{}
, "Object must be derived from QObject"
);
using
object_type =
Object;
using
reference =
object_type &
;
using
const_reference =
object_type const
&
;
using
pointer =
object_type *
;
using
const_pointer =
object_type const
*
;
explicit
ScopedQPointer(Object *
p =
nullptr
) : qptr{
p}
{}
ScopedQPointer(ScopedQPointer const
&
) =
delete
;
ScopedQPointer &
operator
=
(ScopedQPointer const
&
) =
delete
;
ScopedQPointer(ScopedQPointer &&
other) Q_DECL_NOEXCEPT
:
qptr{
other.release() }
{
}
ScopedQPointer &
operator
=
(ScopedQPointer &&
other) Q_DECL_NOEXCEPT
{
ScopedQPointer{
std::
move(other)}
.swap(*
this
);
return
*
this
;
}
ScopedQPointer &
operator
=
(pointer p) {
reset(p);
return
*
this
;
}
~
ScopedQPointer() {
Deleter{}
( qptr.data() );
}
bool
isNull() const
{
return
qptr.isNull(); }
void
swap(ScopedQPointer &
other) Q_DECL_NOEXCEPT
{
qptr.swap(other.qptr); }
void
reset(pointer p =
nullptr
) {
ScopedQPointer tmp{
p}
;
swap(tmp);
}
QPointer
<
object_type>
release() {
auto
ret =
qptr;
qptr.clear();
return
ret;
}
pointer data() const
{
return
qptr.data(); }
pointer operator
->
() const
{
return
data(); }
reference operator
*
() const
{
return
*
data(); }
operator
pointer () const
{
return
data(); }
private
:
QPointer
<
object_type>
qptr;
}
;
struct
LateDeleter {
template
<
class
Object>
void
operator
() (Object *
p) const
{
if
(p) {
p->
deleteLater();
}
}
}
;
template
<
class
Object>
using
ScopedQPointerDeleteLater =
ScopedQPointer<
Object, LateDeleter>
;
#endif
// SCOPED_QPOINTER_HPP
Lien : FAQ C++ : pointeurs intelligentsFAQ C++ : pointeurs intelligents
Lien : Les pointeurs intelligents, de Loïc Joly
Faire un delete sur un QObject peut être très dangereux. Par exemple, avec le système de signaux et de slots, il est parfois difficile d'être sûr qu'il n'est pas utilisé par la pile d'appels au moment du delete : on veut détruire un bouton quand on clique dessus.
class
widget : public
QWidget
{
Q_OBJECT
QPointer
<
QPushButton
>
m_but ;
public
:
widget ()
{
m_but =
new
QPushButton
(this
);
connect
(m_but, SIGNAL
(clicked()), this
, SLOT
(butClicked()));
}
public
slots
:
void
butClicked()
{
delete
m_but;
m_but =
0
;
}
}
;
Lorsque l'on appuie sur le bouton, le signal clicked est émis, ce qui appelle directement le slot butClicked(). À ce niveau, la pile d'appels correspond à :
widget => butClicked()
m_but => clicked()
Ainsi, le bouton sur lequel on veut faire un delete est utilisé par la pile d'appels. Le delete va produire une erreur mémoire et votre application se retrouve en état indéterminé.
Pour cela, QObject fournit la fonction deleteLater, qui va poster un événement et le QObject sera détruit par la boucle d'événements dès que tout danger est écarté.
La fonction devient tout simplement :
void
butClicked()
{
m_but->
deleteLater();
}
Il est intéressant de constater que cette fonction est un slot et peut donc être connectée à un signal.
Il est aussi possible d'appeler plusieurs fois cette fonction sans aucun danger.
Lorsque l'on utilise un QObject (ou un enfant) en membre d'une classe, on peut se demander s'il faut utiliser un pointeur ou non. Voici quelques conseils.
- Objet optionnel : si l'objet est optionnel, il est fortement conseillé d'utiliser le pointeur intelligent QWeakPointer, qui permet de savoir si le QObject pointé existe (contrairement à QPointer).
- Les objets partagés entre classes : les QObject ne sont pas prévus pour être utilisés par plusieurs classes en même temps. Le problème ne devrait donc pas se poser.
- Les QWidget : comme ils peuvent être reparentés (par les dispositions, par exemple), il est fortement conseillé d'utiliser des pointeurs pour garantir leur destruction de manière correcte.
- Les autres : au choix. Il peut être intéressant de ne pas utiliser de pointeur pour éviter de faire les allocations à la main. Ceci ne pose aucun problème avec le système de destruction parent/enfant, car ils seront détruits avant la classe. Toutefois, il ne faut pas oublier de mettre this en parent.
class
monObj : QObject
{
QTimer
m_timer;
public
:
monObj ()
:
m_timer(this
)
{
}
}
class
monObj : QObject
{
QTimer
m_timer;
public
:
monObj ()
{
m_timer.setParent(this
);
}
}
Il est très important que tous les QObject en membre d'une classe aient pour parent this. QWidget est un cas particulier où le parent dépend de son niveau dans l'arbre parent/enfants des widgets et où le parent est forcément un QWidget.