URL: https://linuxfr.org/users/small_duck/journaux/tirez-vous-une-buche-qu-on-cause-c-et-singletons Title: Tirez-vous une bûche, qu'on cause C++ et singletons Authors: small_duck Date: 2018年08月14日T23:48:16+02:00 License: CC By-SA Tags: c++ Score: 17 Le métier est venu me voir. "On aime beaucoup ton composant A, celui qui cache les données depuis la base, mais on voudrait l'instancier tout plein de fois, sans qu'il recharge tout à chaque fois." Comme j'aime bien le métier, je me suis attelé à la tâche, je me suis bouché le nez, et je leur ai planqué une petite variable globale des familles histoire de mettre en commun les caches. Et ils étaient fort contents. Ensuite, j'ai regardé ma variable de plus près, et je me suis rendu compte avec horreur que je venais d'implémenter un [singleton](https://fr.wikipedia.org/wiki/Singleton_(patron_de_conception)) ! Miséricorde. Chacun sait que le singleton canonique est un anti-patron : - Il est difficile à rendre réentrant - Sa destruction est difficile à contrôler - Il ne joue pas bien du tout avec les tests unitaires - Et enfin, non seulement c'est une variable globale, mais c'est une variable globale que l'on ne peut pas rendre locale : on ne peut pas instancier l'objet localement, puisque son constructeur est caché. Sauf que je dois être dans un des rares cas qui marchent. En effet, mon cache n'a pas "besoin" d'être une variable globale, et il est parfaitement correct de considérer une instance comme étant indépendante. Le fait que les instances soient partagées est un détail d'implémentation qui correspond à une exigence non-fonctionnelle de performance ou de consommation mémoire. Si l'utilisateur instancie vraiment un objet séparé, ce n'est pas grave, je n'ai donc pas besoin de rendre mon constructeur privé. Ensuite, en utilisant des pointeurs "weak", je m'assure que l'objet est vraiment détruit quand mon utilisateur a fini de s'en servir, et j'évite donc que l'objet survive au delà de ma séquence de destruction. Bonus, cela m'assure une destruction totale à la fin de chaque test unitaire. Et enfin, en faisant un peu attention, la réentrance n'est pas si méchante. Forcément, j'ai eu envie de le rendre générique, ce qui m'a permis d'utiliser des paramètres variadiques, ce qui est toujours plaisant. Je vous présente donc l'engin: ```c++ #include #include #include // Le type générique singleton template class Singleton { public: // Constructeur variadique prenant les paramètres permettant de construire // l'objet si besoin est template std::shared_ptr create(ARGS ... args) { // On protège notre pointeur weak std::lock_guard lock(_mutex); // Est-ce que le pointeur weak pointait déjà sur un pointeur strong? auto strong = _weak.lock(); if(!strong) { // Non... Créons le strong = std::make_shared(args...); // Et assignons le pointeur weak _weak = strong; } // Retournons soit l'objet déjà existant, // soit le nouveau que l'on vient de créer. return strong; } private: std::mutex _mutex; // Comme l'on utilise un weak_ptr, l'objet sera détruit // lorsque sera détruite la dernière instance utilisateur. std::weak_ptr _weak; }; // Classe de test class A { public: A(int, double) { } }; // Factory pour le type A std::shared_ptr createA(int a, double b) { // Astuce ! Une variable définie comme "static" au sein // d'une fonction est en réalité globale, mais accessible // seulement de cette fonction. Elle sera initialisée au // premier appel de cette fonction, de manière thread-safe. static Singleton singletonA; return singletonA.create(a, b); } // Test int main() { auto a = createA(1, 2); auto b = createA(1, 2); assert(a == b); return 0; } ``` Après, l'on peut avoir besoin de créer des objets différents en fonction des paramètres d'entrée, et dans ce cas il faudrait plutôt utiliser un conteneur associatif pour garder les pointeurs weak (et la généralisation pourrait être plus ardue). Enfin, l'on notera que cette technique ne marche que si l'utilisateur garde longtemps ses pointeurs sur A. S'il appelle createA, utilise l'objet de retour, et le détruit immédiatement, le cache sera perdu. En revanche, si A est voué à être gardé comme variable membre de plusieurs composants indépendants qui seront disponibles la majeure partie du temps de vie de l'application, le cache fonctionnera à plein.

AltStyle によって変換されたページ (->オリジナル) /