Class ayant comme attribut un objet

Le problème exposé dans ce sujet a été résolu.

Bonjour à tous,

j'étais en train de coder en POO avec Qt et j'ai remarqué que souvent quand une class a comme attribut un objet on met un pointeur vers l'objet et pas directement l'object.

Ma question est donc pourquoi cela et il y a t'il une différence notable entre les 2 pour que l'on utilise une manière plutôt qu'une autre.

J'espère que j'ai été clair sur ma question sinon faite le moi savoir mais je ne savais pas trop comment l'expliquer.

+0 -0

Salut,

si je me rappelle bien, Qt utilise un système de parents-enfants pour gérer la mémoire. Si tu mets l'objet directement dans la classe (et que tu lui indique un parent, ou qu'il est mit dans un layout, …), il pourrait y avoir "tentative de double libération de la mémoire" (une fois le destructeur de l'objet appelé à la fin d'un scope, une fois deleté par le parent). Mais j'en suis pas du tout sûr. Si je raconte n'importe quoi, corrigez moi :p .

+0 -0

(il faut également que l'objet soit le parent du second. Mais effectivement, cela produit un double delete).

En complément, les classes dérivant de QObject ne sont pas copiable, ce qui fait qu'elles sont souvent manipulées via pointeurs.

+0 -0

Salut,

Pour moi la raison encore la plus simple et évidente à comprendre étant donné que les classes de QT sont fortement organisées en hiérarchies (le parent possède des enfants et chaque enfant possède des références vers ses parents), c'est que définir la classe QObject de cette manière serait impossible :

1
2
3
4
5
class QObject {
private:
QObject parent;
//...
};

En effet, avec cette définition, la construction d'un QObject nécéssite elle-même la construction d'un QObject, qui nécéssite elle-même la construction d'un QObject, qui nécéssite elle-même la construction d'un QObject, ...... et dans 100 ans on y est encore.

La seconde possibilité qui paraît la plus logique est d'utiliser une référence :

1
2
3
4
5
class QObject {
private:
QObject& parent;
//...
};

Ici, la construction d'un nouveau QObject ne nécéssite pas la construction d'un QObject, il n'y a donc pas de boucle sans fin. Mais pour pouvoir construire un nouveau QObject, il faut posséder une référence vers un QObject déjà existant, car les références ne peuvent pas être nulles (elles doivent obligatoirement pointer sur un objet valide)1. On se retrouve donc dans l'impossibilité de construire le premier objet, puisqu'à ce stade il n'existe pas encore d'autre objet auquel on peut faire référence.

ON n'a donc pas le choix, on est obligé d'utiliser un pointeur :

1
2
3
4
5
class QObject {
private:
QObject* parent;
//...
};

Dans ce cas aucun problème, le pointeur peut ne pointer sur rien (nullptr). Habituellement on préfèrera un pointeur intelligent genre shared_ptr (pas unique_ptr dans ce cas ici!) plutot qu'un pointeur nu.


  1. En réalité, rien ne nous interdit de passer *this ou *0, ça ne plantera pas tant qu'on n'utilise pas effectivement la référence. Mais inutile de dire que c'est source de nombreuses erreurs soit de boucles sans fin soit de plantages, car normalement on part du principe que quand on utilise une référence, elle référence toujours un objet valide dans tous les cas. A ne réserver donc qu'aux experts avertis ! 

+0 -0

Cela change la manière dont sont organisées les données en mémoire et par conséquence ce qu'on peut ou ne peut pas faire avec, et les performances des accès aux données. Quand j'écris :

1
2
3
4
5
6
7
8
class B{
  int j;
};

class A{
  int i;
  B b;
};

A contient un A directement un B. Donc un A, c'est d'abord un int, et immédiatement derrière en mémoire un B, qui contient un int. Donc si on regarde la taille d'un "A", c'est la taille d'un int + la taille d'un B (qui fait la taille d'un int).

De plus comme pour calculer la taille A, je suis obligé de connaître la taille d'un B, je suis obligé de savoir quelle est la définition exacte de la classe B (sinon je ne sais pas calculer sa taille).

En revanche, si j'écris :

1
2
3
4
5
6
class B;

class A{
  int i;
  B& b;
};

A c'est un int suivi d'une référence vers un B. Donc la taille de A, c'est la taille d'un int + la taille d'une référence. Or la taille d'une référence, c'est fixe, donc je n'ai pas besoin de savoir quelle est la définition exacte de B, juste que c'est un type qui existe.

Ok merci, donc le fait de mettre une référence ou un pointeur permet aux autres développeurs de connaître la taille prise en mémoire par la class sans connaître les types quelle contient.

Juste tu dis que cela change aussi les perfs d'accès aux données donc l'une des 2 méthodes est plus rapide ?

+0 -0

Ok merci, donc le fait de mettre une référence ou un pointeur permet aux autres développeurs de connaître la taille prise en mémoire par la class sans connaître les types quelle contient.

LudoBike

Pas exactement. C'est plutôt que comme ce que tu stockes, c'est une adresse et pas l'objet lui-même, le compilateur n'a pas besoin de savoir la taille de l'objet puisqu'il est ailleurs en mémoire. Il a juste besoin de laisser l'espace nécessaire au stockage de la référence.

Le développeur, lui il s'en tape à moins qu'il soit en train de faire de l'horlogerie fine.

Après cela à un autre effet, c'est que sémantiquement, cela n'a pas la même signification :

1
2
3
4
5
6
7
8
class A{
  B  b1;
  B& b2;
  B* b3;
  std::reference_wrapper<B> b4;
  //à venir dans le futur, implémentable à l'heure actuelle et dispo dans experimental :
  std::optionnal<B> b5
};

b1 est un objet dont la durée de vie est exactement égale à la durée de vie de l'objet de type "A" qui l'englobe. Par conséquent, il est valide pendant toute la vie de A, et devient invalide dès sa destruction. Cet objet sera le même pendant toute la vie du A.

b2 accède à un objet dont la durée de vie est supérieure ou égale à la durée de vie de A. Il doit être valide avant la création du A et peut rester valide après la mort du A. Cet objet sera le même tout la vie du A.

b3 accède à un objet dont la présence est optionnelle, de plus il peut être changé. La durée de vie de l'objet pointé n'est pas liée à celle du A, mais on doit toujours y trouver un objet valide. Donc évidemment, c'est le plus dur à gérer.

b4 accède à un objet dont la présence est obligatoire. En revanche, il peut être changé pour pointer ailleurs. On doit toujours y trouver un objet valide, il est à peine plus facile à gérer qu'un pointeur nu.

b5 est équivalent à b3 mais sa sémantique est plus claire à la lecture.

Juste tu dis que cela change aussi les perfs d'accès aux données donc l'une des 2 méthodes est plus rapide ?

LudoBike

Quand une variable membre est directement un objet, cet objet se trouve dans le même espace mémoire que son objet englobant, donc si on y accède la mémoire est localisée très proche* . En revanche, si on a un pointeur ou une référence, l'objet peut se trouver ailleurs en mémoire, l'accès pouvant être plus long. * Mais il est important de noter que c'est vrai récursivement, donc si on stocke directement un vector, on connaît gratuitement sa taille, sa capacité, et le point d'accès aux données, mais ces données, elles, sont ailleurs.

Ok merci, donc il est préférable de mettre un pointeur dans la plupart des cas.

LudoBike

b3 accède à un objet dont la présence est optionnelle, de plus il peut être changé. La durée de vie de l'objet pointé n'est pas liée à celle du A, mais on doit toujours y trouver un objet valide. Donc évidemment, c'est le plus dur à gérer.

Ksass`Peuk

T'es masochiste ?

Dans la plupart des cas, il est préférable que la durée de vie et la validité de l'objet accédé soit la plus prévisible possible. Si on pouvait on mettrait toujours les objets dedans mais en vérité on peut pas toujours, donc on passe par des références. Et quand vraiment on a envie de pouvoir changer l'objet en question, on passe sur le reference_wrapper, et si là encore on trouve que c'est trop restrictif, on passe sur le pointeur nu. Mais dans cet ordre. Et on motive les choix.

Et pour le cas de Qt, la motivation du choix, c'est : Qt nous dit de faire ça. Mais hors de Qt, on s'en passe très bien.

Bah pareil que quand tu as un objet, sauf qu'à l'initialisation, au lieu de paramétrer le constructeur de l'objet, tu vas passer une référence à l'attribut :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct B{
  B(int i);
};

class A{
private:
  B  m_b1;
  B& m_b2;
  B* m_b3;

public:
  A(int i, B& b2, B& b3) : 
    m_b1{ i }, 
    m_b2{ b2 },
    m_b3{ &b3 }
  {}
};

Ben non, c'est la référence qui y est encapsulée (mais c'est la même chose pour le pointeur).

L'encapsulation c'est un moyen de maintenir les invariants de ta classe valides. L'encapsulation n'est ni un objectif, ni un but.

Le fait est que si tu as une référence mutable vers un objet externe. Soit l'invariant de ton objet englobant ne dépend pas de l'état de l'objet référencé (juste de sa validité). Soit l'invariant de ton objet englobant en dépend, et dans ce cas, celui qui te fournit l'objet doit s'engager à te donner une garantie qu'un certain nombre de propriétés à son sujet sont respectée.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte