I. L'article original

Cet article est une adaptation en langue française de Implicit / Explicit Data Sharing with Qt.

II. Partage implicite de données

Le patron de conception du poids-mouche est souvent utilisé dans Qt. Un poids mouche (flyweight) est un objet qui minimise l'utilisation mémoire en partageant autant de données que possible avec d'autres objets similaires. Qt utilise de manière extensive ce patron mais y fait référence sous le nom de partage implicite.

Beaucoup de classes de Qt (comme QString, QList, QHash...) utilisent un partage implicite des données pour maximiser l'utilisation des ressources et minimiser les copies. Une classe partagée contient essentiellement un pointeur intelligent sur un bloc partagé de mémoire. Par voie de conséquence, il est sécurisé et efficace de les copier (copie superficielle) et de les passer comme arguments de méthodes.

Les données pointées par la classe ne sont copiées (copie en profondeur) que quand les deux conditions suivantes sont remplies :

  • le compteur de référence est strictement supérieur à l'unité (on a donc au moins deux copies du poids-mouche) ;
  • une méthode essaye d'écrire dans le bloc partagé de mémoire.

III. Quand utiliser le partage implicite ?

Le partage implicite est particulièrement utile quand on utilise beaucoup d'objets identiques, quand ils sont souvent partagés ou copiés. Par exemple, pour représenter en mémoire des notifications sociales de Facebook, on a beaucoup de posts différents écrits par un nombre plutôt limité de contacts (les amis). Utiliser le partage implicite de données pour la classe de contact économisera de la mémoire, puisque ce sont les mêmes contacts qui seront utilisés dans différentes notifications. On pourrait également utiliser des pointeurs bruts pour le même effet, mais la gestion de la mémoire devient plus délicate (quand faut-il détruire un objet de contact ?).

IV. Comment implémenter le partage implicite de données ?

Qt rend l'implémentation du partage implicite très facile par ces deux classes :

On considère l'exemple d'une classe Contact. Tout d'abord, un considère une surcouche légère (le poids-mouche) : la classe Contact. Simplement, il s'agit d'un QSharedDataPointer sur un objet ContactData. La classe ContactData sera définie plus tard dans un en-tête séparé (et privé). Parce que l'on choisit de ne pas déclarer ContactData dans l'en-tête, on doit effectuer une déclaration avancée. On aura alors quelques obligations pour la classe Contact, on devra définir :

  • un destructeur pas inliné ;
  • un constructeur par copie pas inliné ;
  • un opérateur d'assignation pas inliné.

Ces méthodes ne devraient pas être inlinées, ce qui signifie que l'on devrait les implémenter dans un fichier CPP séparé (voir plus bas).

contact.h
Sélectionnez
#include <QSharedDataPointer>
#include <QDateTime>
 
// Déclaration avancée
class ContactData;
 
class Contact
{
public:
    Contact(); // Obligatoire
    Contact(const Contact &other); // Obligatoire
    ~Contact(); // Obligatoire
    Contact& operator=(const Contact& other); // Obligatoire
    // Accesseurs en lecture
    QString displayName() const;
    QDateTime birthDate() const;
    // Accesseurs en écriture
    void setFirstName(const QString& first_name);
    void setLastName(const QString& last_name);
    void setBirthDate(const QDateTime& date);
 
private:
    QSharedDataPointer<ContactData> d;
};

En voici l'implémentation. Rien de difficile ou particulier, on implémente simplement les méthodes de la classe. L'en-tête privé contact_p.h doit être inclus dans le fichier cpp (et non dans contact.h), car il ne sera pas installé sur le système de destination (le but étant de le cacher).

contact.cpp
Sélectionnez
#include "contact.h"
#include "contact_p.h" // L'en-tête privé
 
Contact::Contact() {}
 
Contact::Contact(const Contact &other):
  d(other.d) {}
 
Contact::~Contact() {}
 
Contact& Contact::operator=(const Contact& other) {
  d = other.d;
  return *this;
}
 
QString Contact::displayName() const {
  return d->first_name+" "+d->last_name;
}
 
QDateTime Contact::birthDate() const {
  return d->birth_date;
}
 
void Contact::setFirstName(const QString &first_name) {
  d->first_name = first_name;
}
 
void Contact::setLastName(const QString &last_name) {
  d->last_name = last_name;
}
 
void Contact::setBirthDate(const QDateTime &date) {
  d->birth_date = date;
}

Finalement, la classe ContactData, qui hérite de QSharedData. Cette classe ne fait que contenir des membres privés, les données. Ces membres sont marqués publics par facilité et parce que cette classe restera privée.

contact_p.h (en-tête privé, s'assurer qu'il ne sera pas installé)
Sélectionnez
#include <QSharedData>
#include <QDateTime>
 
class ContactData: public QSharedData {
public:
  ContactData() {}
 
  QString first_name;
  QString last_name;
  QDateTime birth_date;
};

C'est tout ! Vous pouvez maintenant utiliser la classe Contact sans vous soucier des pointeurs, de la gestion de la mémoire ou de l'efficacité en copie.

V. Partage explicite de données

Pour certaines classes, on pourrait vouloir partager les données de manière explicite. Dans le partage explicite, les données partagées ne sont jamais copiées (c'est-à-dire qu'il n'y a pas de fonctionnalité de copie sur écriture - COW, copy-on-write). L'implémentation est presque identique, on doit juste utiliser un QExplicitlySharedDataPointer au lieu de QSharedDataPointer.

Si l'on considère le cas où la classe Contact possède un identifiant, le partage explicite des données peut avoir un sens. Avec l'exemple suivant :

 
Sélectionnez
Contact c1(1000);
c1.setName("Bob");
Contact c2 = c1;
c2.setName("Mike");

Avec un partage implicite, une copie est effectuée à la dernière ligne. Après ce point, on a deux contacts en mémoire avec le même identifiant mais des noms différents. Ce n'est probablement pas ce qui est voulu.

Avec un partage explicite, c1 et c2 partagent le même espace en mémoire et il n'y a pas de copie à l'écriture. La dernière ligne change partout le nom du contact d'identifiant 1000. Par voie de conséquence, c1 et c2 auront le même nom, Mike, après la dernière ligne (tout en ayant l'identifiant 1000).

VI. Considérations de multithreading

La classe QSharedData fournit un comptage de référence thread-safe : on peut partager en toute sécurité les classes entre les threads. Cependant, QSharedData ne fournit aucune garantie sur le fait que les données soit réellement partagées. En conséquence, on devra toujours prendre soin de la thread safety pour les membres à données partagées.

VII. Remerciements

Merci à Guillaume Belz et Maxime Gault pour leur relecture attentive !