Partage
  • Partager sur Facebook
  • Partager sur Twitter

[Qt] Pourquoi des pointeurs ?

Sujet résolu
9 mai 2010 à 15:59:11

Bonjour,

J'ai remarqué que la plupart des personnes utilisant Qt allouent dynamiquement la mémoire de leur widgets à l'intérieur d'une classe principale :

// myClass.hpp
class Window : public QMainWindow
{
    public:
    Window();

    private:
    QMdiArea *_centralWidget;
}

// myClass.cpp
Window::Window()
{
    _centralWidget = new QMdiArea;
    setCentralWidget(_centralWidget);
}


Je ne comprends pas à quoi ça sert, le code suivant marche tout aussi bien :

// myClass.hpp
class Window : public QMainWindow
{
    public:
    Window();

    private:
    QMdiArea _centralWidget;
}

// myClass.cpp
Window::Window()
{
    setCentralWidget(&_centralWidget);
}


Je n'ai absolument aucune idée de ...pourquoi ? Quelqu'un pourrait-il m'éclairer ?
  • Partager sur Facebook
  • Partager sur Twitter
Anonyme
9 mai 2010 à 16:09:12

Tous les objets issus d'une classe à sémantique d'entité (dérivé de QObject) doivent être manipulé avec des pointeurs, c'est d'ailleurs pour ça que le constructeur de copie de QObject est privé. Cette règle ne s'applique pas aux classes à sémantiques de valeur. Ici un QMdiArea n'est pas une valeur (ce n'est pas un int, QDate, QColor, etc.) mais bien une entité, un objet unique en mémoire.

Tu remarqueras d'ailleurs que Qt utilise cette logique partout, dans ses fonctions les objets d'entités sont envoyés par pointeur et les objets de valeurs par références constantes.
  • Partager sur Facebook
  • Partager sur Twitter
9 mai 2010 à 16:25:14

Mais ça n'explique pas pourquoi une classe à sémantique d'entité doit être manipulée avec des pointeurs...Pourquoi le monde a décidé tout d'un coup que les classes à sémantiques d'entités seront manipulées par les pointeurs ? Il y a forcément eu un moment un problème épineux qui a fait réfléchir sur la question, non ?
  • Partager sur Facebook
  • Partager sur Twitter
9 mai 2010 à 16:30:28

pour éviter la copie justement. "copier" une fenêtre ne veut absolument rien dire.
  • Partager sur Facebook
  • Partager sur Twitter
Anonyme
9 mai 2010 à 16:41:27

D'ailleurs il s'avère souvent nécessaire d'allouer dynamiquement les objets (cela implique donc de les manipuler via des pointeurs), prenons ce cas de figure :

Window::Window()
{
	QVBoxLayout layout;
	
		QPushButton button1("Button 1");
		QPushButton button2("Button 2");
		QPushButton button3("Button 3");
	
	layout.addWidget(&button1);
	layout.addWidget(&button2);
	layout.addWidget(&button3);
	
	setLayout(&layout);
}


Si tu affiche une instance de cette classe, tu verras une fenêtre totalement vide, sans layout et sans bouton. La raison est que les objets ont été détruits à la fin du constructeur, car tout objet est détruit à la fin du bloc d'instructions dans lequel il a été déclaré. Or les objets doivent persister en mémoire après la construction de l'objet : il faut utiliser l'allocation dynamique.

Bien sûr ce problème n'apparait pas quand l'objet est en attribut de la classe, car il existera toujours tant que l'objet existera. Sauf qu'on ne va pas tout mettre en attribut non plus, dans l'exemple ci-dessus on aurait très bien pu mettre les boutons et le layout en attribut, mais ça aurait été inutile car ils ne sont pas accédés ailleurs dans la classe, ça encombrerait la définition de la classe plus qu'autre chose…
  • Partager sur Facebook
  • Partager sur Twitter
9 mai 2010 à 18:26:06

C'est un choix propre à Qt. Dans les cas général, on n'est pas obligé d'utiliser les pointeur pour des classes à sémantique d'entité. L'interdiction de la copie et l'utilisation des pointeurs ne sont pas liés à ce point.

@metayd1: Je connais peu Qt, mais si j'ai le choix et aucune raison de le faire, je me passerais de l'allocation dynamique, ca complexifie le code plus qu'autre chose.
  • Partager sur Facebook
  • Partager sur Twitter
FaQ : Fr | En 1 2 | C++11 | Template || Blog : Deloget | C++|Boost--Dev | C++Next | GotW || Installer Boost
9 mai 2010 à 19:09:13

Sa aussi un avantage: c'est que tu a souvent besoin de donner un pointeur avec Qt

Donc sa t'évite d'écrire le '&' a chaque fois. Un oublie est vite arrivé.
  • Partager sur Facebook
  • Partager sur Twitter
Sois ce que tu codes, codes ce que tu es.
9 mai 2010 à 19:11:33

Merci de vos réponses, je ne suis pas sûr d'avoir tout compris, mais tant pis, j'essaierais avec une méthode et je verrais bien.
  • Partager sur Facebook
  • Partager sur Twitter
9 mai 2010 à 23:23:04

En fait, ça pose juste un problème lors de la destruction de ton QObject: Qt va détruire les QObject dans l'ordre hiérarchique et le destructeur de ta classe (la partie automatique) va détruire ses membres dans l'ordre inverse de leur déclaration.
Par exemple si tu déclares un QLineEdit avant un QGroupBox qui le contient, lors de la destruction des membres de ton objet Window, le QGroupBox va être détruit en premier et tenter de détruire le QLineEdit parce que c'est un des ses objets enfant, puis le destructeur de Window va lui aussi tenter de détruire ce même QLineEdit => double destruction => plantage.

Donc on peut le faire, mais il faut déclarer les membres non-pointeurs dans un certain ordre: les objets parents (y compris les QLayout) avant les objets qu'ils contiendront, afin que les enfants soient détruits en premiers et que les parents n'aient plus d'enfants au moment de leur destruction.

Ne pas utiliser l'allocation dynamique peut aussi être plus long à la compilation parce que tu dois mettre des #include pour chaque widget (ou QtGui) dans le fichier d'entête au lieu des déclarations anticipées (mais si tu utilises les entêtes précompilées comme tout le monde le devrait, ça ne change rien).
  • Partager sur Facebook
  • Partager sur Twitter
10 mai 2010 à 11:00:22

Citation : minirop

pour éviter la copie justement. "copier" une fenêtre ne veut absolument rien dire.


Le passage par référence ne copie pas l'objet.
  • Partager sur Facebook
  • Partager sur Twitter
10 mai 2010 à 11:17:47

Citation : BoudBoulMan

Citation : minirop

pour éviter la copie justement. "copier" une fenêtre ne veut absolument rien dire.


Le passage par référence ne copie pas l'objet.



Je confirme :D
  • Partager sur Facebook
  • Partager sur Twitter
10 mai 2010 à 17:56:42

Salut
J'ai longuement réfléchie a ta question

Je vais essayer de répondre simplement, mais bilbax la très bien expliqué

Si tu déclare une variable dans un fonction, a la fin de cet fonction, elle est détruite
int main(int argc, char **argv)
{
int age=10;
return 0;
}



Dans un code comme ça à une seule fonction, tu t'en fou que la variable soit détruit

Mais pour Qt, par exemple, si je crée une variable QLineEdit est que je veux le modifier dans un slot par exemple:



Fenetre::Fenetre()
{
QLineEdit lineEdit;
//...

connect(quitter, SIGNAL(textChanged(QString)),this, SLOT(slot()));//Un connect quelconque, on s'en fout pour l'explication
}


La j'appele ma fonction:


void Fenetre::slot()
{
lineEdit.text();

}



Et la... MINCE alors, je veux récupéré le text de lineEdit, mais il a été détruit car une variable ne marche que pour une seule fonction



Et la tu vois l'importance. Tu à deux solution.

Soit dans un .h tu déclare tes variables, dans ce cas la aucun probleme

Soit tu utilise l'allocation dynamique comme ceci:

Fenetre::Fenetre()
{
QLineEdit lineEdit = new QLineEdit;
//...

connect(quitter, SIGNAL(clicked()),qApp, SLOT(slot()));//Un connect quelconque, on s'en fout pour l'explication
}



Et pour mon slot:


void Fenetre::slot()
{
lineEdit->text();

}




Et la, magie, sa marche


J'espère avoir été claire, si tu a des choses que tu comprend pas, dit le moi, je t'aiderai volontier si je le peux
  • Partager sur Facebook
  • Partager sur Twitter
Sois ce que tu codes, codes ce que tu es.
10 mai 2010 à 18:41:06

Non, non, je comprends l'utilité des pointeurs pour l'allocation dynamique, la question que je me posais c'était uniquement pour les attributs de classe (dans une classe Fenetre par exemple), qui n'ont eux aucune utilité d'être des pointeurs je trouve...

  • Partager sur Facebook
  • Partager sur Twitter
10 mai 2010 à 18:46:19

A ok^^
Je me suis embété pour rien

La seule explication que je vois est la suivante:

Souvent dans Qt tu à besoin d'envoyer un pointeur

L'exemple est pour la fonction connect, tu doit envoyer deux pointeur d'objet

Il y a deux solution, soit tu crée des attribut normaux


Cela donne:

connect(&quitter, SIGNAL(textChanged(QString)),this, SLOT(slot()));



Tu doit mettre le &

C'est pour cela que la plupart utilise des pointeurs, car cela évite de mettre le &

Comme beaucoup de fonction demande des pointeurs, cela permet d'éviter de faire des oublis et alléger le code

Mais je pense que la raison principal est que mathéo utilise dans son cours les pointeurs sans trop expliqué l'intérêt et que tout le monde fait bêtement pareil ;)
  • Partager sur Facebook
  • Partager sur Twitter
Sois ce que tu codes, codes ce que tu es.
11 mai 2010 à 3:32:43

alexisdm, t'as donné la réponse, je vais essayer de la reformuler, sans vraiment connaitre Qt. Le cas où tu doit utiliser des pointeurs est celui où les objets ont un parent, car dans ce cas, Qt utilise un système qui lui est propre où la destruction est la responsabilité du parent, or si tu passes par un attribut classique, il va aussi y avoir une destruction automatique par le destructeur de la classe, ce qui peut poser des problèmes (double destruction). (J'essayerai de faire un code pour illustrer ceci, et vérifier dans la doc de Qt au passage)

@pingloveur: Ne dit "la seule explication" qand tu ne sais pas. De plus, les raisons de ce genre (éviter un caractère), sont rarement les bonnes pour expliquer une solution au niveau d'une architecture logiciel.
  • Partager sur Facebook
  • Partager sur Twitter
FaQ : Fr | En 1 2 | C++11 | Template || Blog : Deloget | C++|Boost--Dev | C++Next | GotW || Installer Boost
11 mai 2010 à 7:02:01

Oui désolé c'était plutot la seule que je voyait
Mais c'est une raison valable celle que j'ai dit même si elle est pas poussé techniquement ;)
Je donnais mon point de vue à partir de mes connaissance.
Moi je sais que j'utilise les pointeurs pour cette raison en se moment
Après je ne savais même pas que c'était obligé pour une autre raison
On en apprend tout les jours ;)

J'édite mon post
  • Partager sur Facebook
  • Partager sur Twitter
Sois ce que tu codes, codes ce que tu es.
11 mai 2010 à 12:44:04

Citation : pingloveur

Après je ne savais même pas que c'était obligé pour une autre raison


Il n'y a aucune obligation, il n'y a que des contraintes sur l'ordre de déclaration des membres non pointeurs.
  • Partager sur Facebook
  • Partager sur Twitter
11 mai 2010 à 12:57:24

Freedom a raison , Qt s'occupe tout seul de la destructions des éléments de l'API Qt , c'est d'ailleurs pour ça qu'il ne faut pas s'occuper de la destruction de ceux-ci.Si tu crée des attributs objets , la mort de la classe risque de foutre la bordel dans l'histoire à cause d'un double delete.
  • Partager sur Facebook
  • Partager sur Twitter
11 mai 2010 à 14:36:08

Après test (sans Qt), ca ne change en faite que l'ordre de destruction, avec des pointeurs l'ordre est linéaire, avec des attributs directs l'ordre est embriqué.

#include <iostream>
#include <cstdlib>
#include <vector>
#include <algorithm>

struct fonc_delete
{
	template<class T>
	void operator()(T* p) { delete p; }
};

class obj
{
	obj* parent;
	std::vector<obj*> collector;
	obj(const obj&) {}
	void operator=(const obj&) {}
	void grab(obj* o) { collector.push_back(o); }
	public:
		obj(obj* p = NULL) 
		{ 
			parent = p;
			if (p != NULL) parent->grab(this);
		}
		virtual ~obj()
		{
			std::cout << 'd';
			std::for_each(collector.begin(), collector.end(), fonc_delete());
			if (parent != NULL)
			{
				std::vector<obj*>& c = parent->collector;
				c.erase(std::remove(c.begin(), c.end(), this), c.end());
			}
		}
};


struct A : obj
{
	A(obj* p = NULL) : obj(p) {}
	~A() { std::cout << 'A'; } 
};

struct B : obj
{
	A a1; A a2;
	B(obj* p = NULL) : obj(p), a1(this), a2(&a1) {}
	~B() { std::cout << 'B'; } 
};
struct C : obj
{
	A* a1; A* a2;
	C(obj* p = NULL) : obj(p) 
	{
		a1 = new A(this);
		a2 = new A(a1);
	}
	~C() { std::cout << 'C'; } 
};

int main()
{
    { B b; }
    system("PAUSE");
    { C c; }
    system("PAUSE");
    return 0;
}

Après je sais pas si Qt fonctionne exactement comme ca, mais c'est ce qui me semble le plus logique. (c'est un code rudimentaire, ca doit surment pourvoir être très largement amélioré, MT par exemple)
  • Partager sur Facebook
  • Partager sur Twitter
FaQ : Fr | En 1 2 | C++11 | Template || Blog : Deloget | C++|Boost--Dev | C++Next | GotW || Installer Boost
11 mai 2010 à 14:56:41

Si on me laisse le choix, je préfére me trimballer des smart_ptr. C'est ce qui m'a toujours gêné avec Qt, jamais être sur de savoir à qui incombe la libération des ressources. (et quoiqu'en dise les puristes de Qt, non c'est pas clair).
Bref, moi j'aurais évité l'overhead et laissé le choix à l'user. (d'un autre côté j'aurais même pas utilisé de god object.... :') )


@freedom : parce que j'aime bien pinailler, l'op() de ton foncteur mériterait d'être const :p.
  • Partager sur Facebook
  • Partager sur Twitter
11 mai 2010 à 15:03:36

@Freedom: Avec un std::set et une fonction removeChildren dans la class obj ce serait beaucoup mieux (le principe de Déméter s'applique aussi entre objets de la même classe) :)

  • Partager sur Facebook
  • Partager sur Twitter
11 mai 2010 à 17:02:06

Sa fait plusieur fois que j'en entant parler
Mais c'est quoi le principe de déméter?
  • Partager sur Facebook
  • Partager sur Twitter
Sois ce que tu codes, codes ce que tu es.
11 mai 2010 à 18:24:45

Bon, ok, les pointeurs sont courants lorsqu'on utilise Qt pour éviter qu'il y ai d'une part le delete de l'attribut lorsque la classe est détruite et d'autre part le delete de Qt qui gère la destruction de tout les attributs enfants de la classe détruite.

Bon, pas besoin d'en savoir plus, cette réponse me suffit amplement pour comprendre !

Merci à tous de vos réponses !
  • Partager sur Facebook
  • Partager sur Twitter
11 mai 2010 à 18:45:46

Mettre les attributs en pointeurs permet d'organiser la construction des objets de manière plus claire. Ainsi que de décider de ne pas en construire certains de manière dynamique à l'exécution.
  • Partager sur Facebook
  • Partager sur Twitter
11 mai 2010 à 19:44:39

Citation : pingloveur

Mais c'est quoi le principe de déméter?

Loi de Déméter (sur Wikipédia)

Dans son exemple, dans le destructeur:
if (parent != NULL)
{
    std::vector<obj*>& c = parent->collector;
    c.erase(std::remove(c.begin(), c.end(), this), c.end());
}
l'objet courant, this, accède au membre "collector" de son parent pour se retirer lui-même de la liste des enfants du parent.
Alors qu'il devrait plutôt demander au parent de le retirer de la liste:
if (parent != NULL)
{
    parent->remove(this);
}
  • Partager sur Facebook
  • Partager sur Twitter
11 mai 2010 à 20:11:08

Merci
C'est vrai que j'aurai du chercher avant de demander ;)
  • Partager sur Facebook
  • Partager sur Twitter
Sois ce que tu codes, codes ce que tu es.
11 mai 2010 à 21:05:26

@goten: j'ai codé ca assez vite, j'ai pas fait attention au const, et je suis du même avis que toi, j'aime pas la facon de Qt de gérer les ressource ni la présence d'une god-class.

@alexisdm: D'après l'article de Emmanuel Deloget (ici), mon code respecte LoD.
  • Partager sur Facebook
  • Partager sur Twitter
FaQ : Fr | En 1 2 | C++11 | Template || Blog : Deloget | C++|Boost--Dev | C++Next | GotW || Installer Boost
11 mai 2010 à 22:07:44

Il ne semble pas y avoir double destruction des QObject enfants, après test :

/* test.cpp */
#include <QObject>
#include <QCoreApplication>
#include <iostream>
#include <cstdlib>

class Child: public QObject
{
	Q_OBJECT
	public:
		Child(QObject *parent = NULL): QObject(parent)
		{
			std::cout << "Construction d'un enfant à " << this << std::endl;
		}
		~Child()
		{
			std::cout << "Destruction de l'enfant à  " << this << std::endl;
		}
};

class Parent: public QObject
{
	Q_OBJECT
	public:
		Parent(QObject *parent = NULL): QObject(parent), child(this)
		{
			std::cout << "Construction d'un parent à " << this << std::endl;
		}
		~Parent()
		{
			std::cout << "Destruction du parent à    " << this << std::endl;
		}

	private:
		Child child;
};

int main(int argc, char **argv)
{
	QCoreApplication application(argc, argv);
	Parent parent;
	return EXIT_SUCCESS;
}

#include "test.moc"


Résultat :
Construction d'un enfant à 0x7fff5400fc20
Construction d'un parent à 0x7fff5400fc10
Destruction du parent à    0x7fff5400fc10
Destruction de l'enfant à  0x7fff5400fc20
  • Partager sur Facebook
  • Partager sur Twitter
11 mai 2010 à 23:02:46

Citation : Freedom

@alexisdm: D'après l'article de Emmanuel Deloget (ici), mon code respecte LoD.


Non, parce que tu accèdes à un niveau trop éloigné de l'objet: this->parent->collector->erase(...). collector n'a pas été créé par this ni donné à this, donc il ne peut pas l'utiliser. Mais parent, appartient à this donc on peut appeler une de ses méthodes.

Citation : spider-mario

Il ne semble pas y avoir double destruction des QObject enfants, après test :


Et il y avait plus simple comme test:
#include <QObject>

int main()
{
   QObject enfant;
   QObject parent;
   enfant.setParent(&parent);
   
}

qui donne:
$ ./test
*** glibc detected *** ./test: double free or corruption (out): 0xbfa3dfa8 ***
parent est détruit avant enfant, donc détruit enfant à cause du setParent, puis enfant est redétruit.
Avec le code qu'on écrirait naturellement, il n'y a pas de problème:
#include <QObject>

int main()
{
   QObject parent;
   QObject enfant(&parent);
}
Parce qu'enfant est détruit avant, et donc retiré de la liste des enfants de parent, puis parent qui n'a plus d'enfant est détruit.
Mais pour les membres d'un objet, on peut les indiquer dans l'ordre qu'on veut et si on ne fait pas attention ça plante.
  • Partager sur Facebook
  • Partager sur Twitter
11 mai 2010 à 23:21:06

Citation : alexisdm

Citation : Freedom

@alexisdm: D'après l'article de Emmanuel Deloget (ici), mon code respecte LoD.


Non, parce que tu accèdes à un niveau trop éloigné de l'objet: this->parent->collector->erase(...). collector n'a pas été créé par this ni donné à this, donc il ne peut pas l'utiliser. Mais parent, appartient à this donc on peut appeler une de ses méthodes.


std::vector<obj*> est un fournisseur préféré potentiel de la méthode ~obj() en tant que classe d'une variable de obj, en tant que tel (et par la loi de Demeter), je peus utiliser son protocole, en particulier le message erase(), begin() et end().

Citation : Emmanuel Deloget (adapté de Lieberherr et Holland)


Loi de Demeter :
Dans toute méthode M, on utilise uniquement le protocole des fournisseurs potentiel préférés de la méthode M.

  • Partager sur Facebook
  • Partager sur Twitter
FaQ : Fr | En 1 2 | C++11 | Template || Blog : Deloget | C++|Boost--Dev | C++Next | GotW || Installer Boost