Stratégie pour cycle de vie d'un objet ressource global

Le
Patrick 'Zener' Brunet
Bonsoir.

Je voudrais obtenir quelques avis d'experts sur un problème épineux.

Considérons que vous livrez une bibliothèque de sources C++ (avec des
templates, comme ça on ne parle pas de forme compilée).
Dans cette bibliothèque se trouve un sous-ensemble qui est un framework de
support pour vos classes, fournissant des services.

Par exemple, imaginons que vous fournissez des gestionnaires de mémoire
particuliers (heaps). De tels objets doivent bien sûr être instanciés avant
tout client et terminés après destruction de tout client. On va supposer en
plus que vous ne voulez pas imposer à votre utilisateur d'invoquer une
fonction Init() et une fonction Cleanup() pour votre bibliothèque.

Donc, et même si certains risquent de dire que c'est sale, une bonne méthode
a priori semble de déclarer les objets de votre framework comme globaux,
afin de bénéficier de l'invocation automatique des constructeurs et
destructeurs Et donc vous les mettez dans le module qui fournit les
fonctions membres de vos classes.

Mais là où ça .erde, c'est que rien ne doit interdire à votre utilisateur
d'utiliser lui-même appel des objets globaux construits à partir de
certaines de vos classes

Et alors, de manière aléatoire selon l'ordre d'assemblage des modules
compilés, les objets de l'utilisateur sont construits avant le framework et
donc l'utilisent à l'état non encore construit !!! Et inversement pour la
destruction finale :-[

Premier élement de solution: je change un peu de stratégie en allouant les
objets du framework dynamiquement, ils sont alors représentés par des
pointeurs static initialisés à NULL, et ils sont créés à la première
tentative d'utilisation.
Pour ça je fais appel à un objet "manager", sans données internes afin que
lui supporte de ne pas avoir été construit, et qui va fournir une méthode
static pour accéder au framework, après l'avoir construit si nécessaire.
Méthode qu'il invoque lui-même dans son constructeur, au cas où le problème
qui nous occupe ne se serait pas posé.

Ainsi c'est bon pour la construction :-)

Mais comment alors gérer la nécessaire destruction du framework ?

On ne peut pas compter sur le destructeur du "manager", car dans le cas qui
fâche, où il est construit trop tard, il sera aussi détruit trop tôt !
Se lancer dans une histoire de lock-count ? Ca veut dire compter sur
l'infaillibilité de son utilisateur :-/

La solution que j'ai trouvée fait appel à la bonne vieille fonction atexit()
de la stdlib, par laquelle je définis une callback pour assurer la
destruction de mon framework à l'exit-time, donc après destruction de tous
les objets globaux (elle invoque en fait une autre méthode du "manager",
static publique mais locale au module, et c'est aussi le "manager" qui la
définit, pour faire un peu propre).

Ca marche très bien, mais je me pose des questions, notamment en cas de
contexte multithread (la bibliothèque elle-même est thread-safe).
A priori, la callback est invoquée lors de la terminaison du thread
principal, c'est à dire après arrêt de tous les autres, et donc destruction
de tous les objets qu'ils ont pu construire si on termine proprement.
Donc ça doit être bon

Qu'en pensez-vous ? Vous faites comment dans ce genre de situation ?
Merci.

--
Cordialement.
--
/**************************************************
* Patrick BRUNET
* E-mail: lien sur http://zener131.free.fr/ContactMe
**************************************************/
Vidéos High-Tech et Jeu Vidéo
Téléchargements
Vos réponses
Gagnez chaque mois un abonnement Premium avec GNT : Inscrivez-vous !
Trier par : date / pertinence
Michael DOUBEZ
Le #313650
Bonsoir.

Je voudrais obtenir quelques avis d'experts sur un problème épineux.

Considérons que vous livrez une bibliothèque de sources C++ (avec des
templates, comme ça on ne parle pas de forme compilée).
Dans cette bibliothèque se trouve un sous-ensemble qui est un framework de
support pour vos classes, fournissant des services.

Par exemple, imaginons que vous fournissez des gestionnaires de mémoire
particuliers (heaps). De tels objets doivent bien sûr être instanciés avant
tout client et terminés après destruction de tout client. On va supposer en
plus que vous ne voulez pas imposer à votre utilisateur d'invoquer une
fonction Init() et une fonction Cleanup() pour votre bibliothèque.

Donc, et même si certains risquent de dire que c'est sale, une bonne méthode
a priori semble de déclarer les objets de votre framework comme globaux,
afin de bénéficier de l'invocation automatique des constructeurs et
destructeurs... Et donc vous les mettez dans le module qui fournit les
fonctions membres de vos classes.

Mais là où ça .erde, c'est que rien ne doit interdire à votre utilisateur
d'utiliser lui-même appel des objets globaux construits à partir de
certaines de vos classes...

Et alors, de manière aléatoire selon l'ordre d'assemblage des modules
compilés, les objets de l'utilisateur sont construits avant le framework et
donc l'utilisent à l'état non encore construit !!! Et inversement pour la
destruction finale :-[


La solution est un idiome courant de C++. L'instantiation des globales
n'est pas spécifiée mais l'intanciation des statiques de fonction oui:
au premier apel de la fonction. Donc il suffit d'utiliser des fonctions
à la place des globales.

MyObject* globale_ressource()
{
static MyObject* my_object=new MyObjext(1,2,3,4);

return my_object;
}

puis:

globale_ressource()->method();

Pour la destruction, tu laisses faire le ramasse-miette.



Premier élement de solution: je change un peu de stratégie en allouant les
objets du framework dynamiquement, ils sont alors représentés par des
pointeurs static initialisés à NULL, et ils sont créés à la première
tentative d'utilisation.
Pour ça je fais appel à un objet "manager", sans données internes afin que
lui supporte de ne pas avoir été construit, et qui va fournir une méthode
static pour accéder au framework, après l'avoir construit si nécessaire.
Méthode qu'il invoque lui-même dans son constructeur, au cas où le problème
qui nous occupe ne se serait pas posé.

Ainsi c'est bon pour la construction :-)

Mais comment alors gérer la nécessaire destruction du framework ?


Nécessaire, c'et beaucoup dire. L'OS s'en occupe tout seul en général.
Les seuls objets qui posent éventuellement problème sont ceux qui spawn
des threads mais ceux là sont en général explicitement gérés (ie.
prévenus de la fermeture du framework).

Pour les cas où cela serait vraiment nécessaire, par exemple pour rendre
une ressource non géré par le système, tu pourrais utiliser un smart
pointer dans la static.


On ne peut pas compter sur le destructeur du "manager", car dans le cas qui
fâche, où il est construit trop tard, il sera aussi détruit trop tôt !
Se lancer dans une histoire de lock-count ? Ca veut dire compter sur
l'infaillibilité de son utilisateur :-/

La solution que j'ai trouvée fait appel à la bonne vieille fonction atexit()
de la stdlib, par laquelle je définis une callback pour assurer la
destruction de mon framework à l'exit-time, donc après destruction de tous
les objets globaux (elle invoque en fait une autre méthode du "manager",
static publique mais locale au module, et c'est aussi le "manager" qui la
définit, pour faire un peu propre).


D'un autre coté, si ton pointeur global est déjà détruit et mis à NULL
(avec un scoped_ptr par exemple), les utilisateurs savent que l'objet et
inutilisable et cela peut être documenté.


Ca marche très bien, mais je me pose des questions, notamment en cas de
contexte multithread (la bibliothèque elle-même est thread-safe).
A priori, la callback est invoquée lors de la terminaison du thread
principal, c'est à dire après arrêt de tous les autres, et donc destruction
de tous les objets qu'ils ont pu construire si on termine proprement.
Donc ça doit être bon...


Rien ne t'assure que les threads se sont terminés en rendant leurs
ressource donc si il tesuffit de savoir que rien ne va invoquer ton
objet, ça a l'air correct. A moins que l'utilisateur de la bibliothèque
ait aussi cette idée, mais bon, là c'est de sa faute :)


Qu'en pensez-vous ? Vous faites comment dans ce genre de situation ?


Ca depend des besoins.

Je pourrais aussi redéfinir l'opérateur new pour les objets qui ont
besoin d'une allocation spécifique, peut être avec un système de
stratégie d'allocation, et ne pas exposer un allocateur global. Enfin,
ça vaudrait vraiment le coup pour les objet à allocation/deallocation
intensive en environment multithread pour éviter la sérialisaion. Pas le
genre de chose que je fais tous les jour.

Michael

James Kanze
Le #313648
On Dec 13, 6:51 pm, "Patrick 'Zener' Brunet"

Michael a déjà dit ce que j'avais à dire, mais un point :
[...]
La solution que j'ai trouvée fait appel à la bonne vieille
fonction atexit() de la stdlib, par laquelle je définis une
callback pour assurer la destruction de mon framework à
l'exit-time, donc après destruction de tous les objets globaux
(elle invoque en fait une autre méthode du "manager", static
publique mais locale au module, et c'est aussi le "manager"
qui la définit, pour faire un peu propre).


Selon la norme, les fonctions inscrites avec atexit sont
appelées au même temps que les destructeurs -- les fonctions
inscrites avant la construction d'un objet statique après sa
destruction, et les fonctions inscrites après la construction
d'un object avant sa destruction. Tu as donc le même problème
qu'avec les constructeurs.

Ca marche très bien,


Tu en es sûr ? Avec toutes les implémentations.

En passant, g++ documente qu'il ne sait pas implémenter ça
correctement sur toutes les plateformes. Sans doute parce que
pour l'implémenter correctement, il faut que le compilateur et
la bibliothèque avec atexit collaborent, et que sur certaines
plate-formes, comme Solaris, la fonction atexit fait partie
d'une bibliothèque livrée avec le système, sur laquelle g++ n'a
pas d'influence. Si tu as testé dans un tel environement, tout
ce que tu as démontré, c'est que ça marche avec une
implémentation non conforme.

mais je me pose des questions, notamment en cas de contexte
multithread (la bibliothèque elle-même est thread-safe). A
priori, la callback est invoquée lors de la terminaison du
thread principal, c'est à dire après arrêt de tous les autres,
et donc destruction de tous les objets qu'ils ont pu
construire si on termine proprement. Donc ça doit être bon...


C'est une question sur laquelle Posix, en tout cas, se tait.
Sans spécifications supplémentaires, je pars du principe que
s'il y a des threads encore actifs autres que celui qui appelle
exit(), lors de l'appel d'exit(), c'est un comportement
indéfini. (Sauf, peut-être, si on peut garantir que ces threads
ne font rien. Par exemple, qu'il font partie d'un thread pool,
et sont en attente qu'on ait besoin d'eux.)

En passant, c'est aussi un argument contre la destruction de
certaines ressources. Appeler le destructeur d'un mutex (qui lui
appelera sans doute pthread_destroy, ou son équivalent Windows)
quand il y a un thread qui y attend, ce n'est pas conseillé.

Qu'en pensez-vous ? Vous faites comment dans ce genre de
situation ?


J'utilise normalement le modèle singleton pour règler la
construction -- dans le cas d'un environement multi-threaded, je
m'arrangerai que la fonction instance() soit appelée au moins
une fois avant l'entrée dans main(), pour éviter la nécessité
des locks dans le singleton (ou sinon, la fonction instance()
renvoie un boost::shared_ptr qui libère le lock lorsque la
dernière instance est détruite). Pour la destruction, en
général, je ne l'appele pas du tout. C'est le système qui
s'occupe de toutes ses ressources (mémoire, mutex, etc.).

--
James Kanze (GABI Software) email:
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34

Patrick 'Zener' Brunet
Le #313611
Bonjour.

"Michael DOUBEZ" 476199a3$0$24073$
[...]
Et alors, de manière aléatoire selon l'ordre d'assemblage des
modules compilés, les objets de l'utilisateur sont construits avant
le framework et donc l'utilisent à l'état non encore construit !!!
Et inversement pour la destruction finale :-[


La solution est un idiome courant de C++. L'instantiation des
globales n'est pas spécifiée mais l'intanciation des statiques de
fonction oui:
au premier apel de la fonction. Donc il suffit d'utiliser des
fonctions à la place des globales.

MyObject* globale_ressource()
{
static MyObject* my_object=new MyObjext(1,2,3,4);

return my_object;
}

puis:

globale_ressource()->method();

Pour la destruction, tu laisses faire le ramasse-miette.



En plus, ça évite aussi d'écrire:

static
MyObject *
CManager::GetObject( void)
{
if( my_object == NULL)
my_object=new MyObjext(1,2,3,4);
return my_object;
}

donc un test à faire à chaque invocation.

Mais par contre, pas de destruction propre possible avec ça...

[...]


Nécessaire, c'et beaucoup dire. L'OS s'en occupe tout seul en général.
Les seuls objets qui posent éventuellement problème sont ceux qui
spawn des threads mais ceux là sont en général explicitement gérés
(ie. prévenus de la fermeture du framework).



Certes, mais si on compile en mode debug, on se prend des tas d'injures
disant que la bibliothèque ne sait pas restituer proprement la mémoire, et
ça gâche la qualité du produit.
Au minimum ça peut conduire à traquer des bugs qui sont en fait
"volontaires", ce qui est toujours désagréable.

Pour les cas où cela serait vraiment nécessaire, par exemple pour
rendre une ressource non géré par le système, tu pourrais utiliser un
smart pointer dans la static.



C'est ce que je voulais dire en parlant de lock-count.
Bon là on perd le bénéfice du test évité dans l'accesseur...

Mais surtout c'est complètement remis en cause si l'utilisateur lui-même
gère mal ses cycles de vie. J'aimerais bien que ma bibliothèque soit un peu
blindée contre ça.

[...]
D'un autre coté, si ton pointeur global est déjà détruit et mis à NULL

(avec un scoped_ptr par exemple), les utilisateurs savent que
l'objet et inutilisable et cela peut être documenté.



Ca va être de l'inside: s'ils se limitent aux heaps par défaut, c'est
supposé être implicite, donc ils doivent pouvoir faire abstraction de tout
le principe des heaps. Alors leur parler dans ce cas du pointeur de cet
objet "heap par défaut" m'ennuie.

En plus il faut qu'ils lisent la doc, qu'ils comprennent la portée du truc,
et qu'ils sachent ensuite faire la différence entre les erreurs notifiées
dans le code de la bibliothèque *par leur faute* et d'éventuels bugs de
cette bibliothèque.
Ca risque de tourner en une justification permanente, alors que mon but est
plutôt de livrer un code stable et irréprochable.

En fait s'il y a un protocole de fin imposé, autant imposer une fonction
Cleanup() à invoquer obligatoirement en dernier, ce serait plus facile à
faire comprendre... Mais ça gâche tout.

Ca marche très bien,



Du moins les premiers tests...

mais je me pose des questions, notamment en cas de
contexte multithread (la bibliothèque elle-même est thread-safe).
A priori, la callback est invoquée lors de la terminaison du thread
principal, c'est à dire après arrêt de tous les autres, et donc
destruction de tous les objets qu'ils ont pu construire si on termine
proprement.
Donc ça doit être bon...


Rien ne t'assure que les threads se sont terminés en rendant leurs
ressource donc si il tesuffit de savoir que rien ne va invoquer ton
objet, ça a l'air correct. A moins que l'utilisateur de la bibliothèque
ait aussi cette idée, mais bon, là c'est de sa faute :)



Je considère que si un thread ne se termine pas correctement en faisant son
ménage, là il y a un bug de l'utilisateur et il assume.
A priori il va se prendre une access violation s'il essaie d'accéder à
l'objet heap une fois détruit, et son debugger lui dira pourquoi ça lui
arrive.

Par contre, si on fait bien les choses, c'est à dire si le contrôleur (dans
le thread principal) d'un thread secondaire lui donne un signal de fin et
*attend* qu'il ait terminé avant de rendre la main, alors tous les threads
se seront arrêtés en ayant fait leur ménage quand on détruira le dernier
contrôleur de thread, et donc je veux que mon heap ne soit pas détruit avant
ça.

Théoriquement, si j'arrive à repousser sa destruction en fin d'exit time du
thread principal, c'est bon.

Par contre James a signalé un problème de synchro qui me remet dans la
.erde:
- les callbacks inscrites par atexit() sont mixées avec les destructeurs
globaux, et pas repoussées en fin de séquence d'exit...
Ca c'est pas terrible comme implémentation :-(

Qu'en pensez-vous ? Vous faites comment dans ce genre
de situation ?


Ca depend des besoins.

Je pourrais aussi redéfinir l'opérateur new pour les objets qui ont
besoin d'une allocation spécifique, peut être avec un système de
stratégie d'allocation, et ne pas exposer un allocateur global. Enfin,
ça vaudrait vraiment le coup pour les objet à allocation/deallocation
intensive en environment multithread pour éviter la sérialisaion.
Pas le genre de chose que je fais tous les jour.



En fait c'est exactement ce que j'ai fait:
- j'ai redéfini les opérateurs new et delete pour qu'ils utilisent mes
heaps, comme les classes de la bibliothèque,
- avec des mécanismes d'exceptions assez sophistiqués etc.,
- le but étant que tout soit complètement transparent pour l'utilisateur,
particulièrement lorsqu'il utilise (implicitement) le heap par défaut.

Donc il n'y a pas de modification imposée du protocole standard, les heaps
apparaissent comme une extension optionnelle (un pointeur de placement pour
le new).
Mais par contre il y a une modification de l'implémentation qui introduit
systématiquement des objets heaps, dont un qui est un wrapper pour le heap
par défaut.
C'est plutôt propre, maintenant je veux que ce soit aussi blindé jusqu'à
l'exit.

Bon, je vais creuser encore.
Merci pour cet avis.


Patrick 'Zener' Brunet
Le #313610
Bonjour.

"James Kanze"
On Dec 13, 6:51 pm, "Patrick 'Zener' Brunet"

Michael a déjà dit ce que j'avais à dire, mais un point :
[...]
La solution que j'ai trouvée fait appel à la bonne vieille
fonction atexit() de la stdlib, par laquelle je définis une
callback pour assurer la destruction de mon framework à
l'exit-time, donc après destruction de tous les objets globaux
(elle invoque en fait une autre méthode du "manager", static
publique mais locale au module, et c'est aussi le "manager"
qui la définit, pour faire un peu propre).


Selon la norme, les fonctions inscrites avec atexit sont
appelées au même temps que les destructeurs -- les fonctions
inscrites avant la construction d'un objet statique après sa
destruction, et les fonctions inscrites après la construction
d'un object avant sa destruction. Tu as donc le même
problème qu'avec les constructeurs.



Pas terrible comme implémentation :-(

Ca marche très bien,


Tu en es sûr ? Avec toutes les implémentations.



J'avais un doute, il est validé :-(

En passant, g++ documente qu'il ne sait pas implémenter ça
correctement sur toutes les plateformes. Sans doute parce que
pour l'implémenter correctement, il faut que le compilateur et
la bibliothèque avec atexit collaborent, et que sur certaines
plate-formes, comme Solaris, la fonction atexit fait partie
d'une bibliothèque livrée avec le système, sur laquelle g++ n'a
pas d'influence. Si tu as testé dans un tel environement, tout
ce que tu as démontré, c'est que ça marche avec une
implémentation non conforme.


OK, le moment "après destruction du dernier objet concerné" retrouve son
statut de non-événement, et le seul moyen de l'aborder reste un lock-count.

Par contre, ce que je peux faire (voir précisions données à Michael), c'est
distinguer le lock-count associé aux utilisations "conscientes" de mes
heaps, faites avec les allocateurs spécifiques dans des objets que je
fournis, de celui correspondant aux new de base.
L'intérêt c'est qu'ainsi, si l'utilisateur oublie un delete dans son code,
ça ne perturbera pas le mécanisme fondé sur les objets globaux...
Voilà qui risque de ressuciter un vieux problème: faire la différence entre
les objets globaux + static et ceux construits sur la pile.

mais je me pose des questions, notamment en cas de
contexte multithread (la bibliothèque elle-même est
thread-safe). A priori, la callback est invoquée lors de
la terminaison du thread principal, c'est à dire après arrêt
de tous les autres, et donc destruction de tous les objets
qu'ils ont pu construire si on termine proprement.
Donc ça doit être bon...


C'est une question sur laquelle Posix, en tout cas, se tait.
Sans spécifications supplémentaires, je pars du principe que
s'il y a des threads encore actifs autres que celui qui appelle
exit(), lors de l'appel d'exit(), c'est un comportement
indéfini. (Sauf, peut-être, si on peut garantir que ces threads
ne font rien. Par exemple, qu'il font partie d'un thread pool,
et sont en attente qu'on ait besoin d'eux.)


Moi je me refuse à faire ça: je le perçois comme un faute de programmation.
C'est le thread principal qui détient l'identité du process, alors s'il crée
des threads secondaires, il doit les stopper avant de sortir:
- signal de stop,
- attente de terminaison propre,
- destruction forcée si ça traîne + rapport d'erreur.
Sinon selon moi on part dans un contexte de lancement de second process, ce
qui est tout à fait différent.

En passant, c'est aussi un argument contre la destruction
de certaines ressources. Appeler le destructeur d'un mutex
(qui lui appelera sans doute pthread_destroy, ou son
équivalent Windows) quand il y a un thread qui y attend,
ce n'est pas conseillé.


Avec ma restriction, c'est aussi une faute.

Qu'en pensez-vous ? Vous faites comment dans ce
genre de situation ?


J'utilise normalement le modèle singleton pour règler la
construction -- dans le cas d'un environement
multi-threaded, je m'arrangerai que la fonction instance()
soit appelée au moins une fois avant l'entrée dans main(),
pour éviter la nécessité des locks dans le singleton


Et si l'utilisateur crée bravement un objet global dont le constructeur
(donc avant le main) lance un thread qui lui-même instancie un objet
utilisant le famaux heap, non encore construit ?
Je crois que le lock est indispensable dès la première utilisation.

(ou sinon, la fonction instance()
renvoie un boost::shared_ptr qui libère le lock lorsque la
dernière instance est détruite). Pour la destruction, en
général, je ne l'appele pas du tout. C'est le système qui
s'occupe de toutes ses ressources (mémoire, mutex, etc.).


On en revient toujours là :-(
En mode debug le système proteste, et il a bien raison.
Il y a un trou d'implémentation là, et c'est pas bô...

Je vais encore creuser.
Merci pour cette expertise.

--
Cordialement.
--
/**************************************************
* Patrick BRUNET
* E-mail: lien sur http://zener131.free.fr/ContactMe
**************************************************/


Michael DOUBEZ
Le #313607
Bonjour.

"Michael DOUBEZ" 476199a3$0$24073$
[...]
Et alors, de manière aléatoire selon l'ordre d'assemblage des
modules compilés, les objets de l'utilisateur sont construits avant
le framework et donc l'utilisent à l'état non encore construit !!!
Et inversement pour la destruction finale :-[
La solution est un idiome courant de C++. L'instantiation des

globales n'est pas spécifiée mais l'intanciation des statiques de
fonction oui:
au premier apel de la fonction. Donc il suffit d'utiliser des
fonctions à la place des globales.

MyObject* globale_ressource()
{
static MyObject* my_object=new MyObjext(1,2,3,4);

return my_object;
}

puis:

globale_ressource()->method();

Pour la destruction, tu laisses faire le ramasse-miette.



En plus, ça évite aussi d'écrire:

static
MyObject *
CManager::GetObject( void)
{
if( my_object == NULL)
my_object=new MyObjext(1,2,3,4);
return my_object;
}

donc un test à faire à chaque invocation.


Mais par contre, pas de destruction propre possible avec ça...



Je ne vois pas en quoi cela enpèche la destruction de l'objet - avec un
smart_pointer par exemple:

MyObject* globale_ressource()
{
static boost::scoped_ptr<MyObject> my_object(new MyObjext(1,2,3,4));

return my_object.get();
}

Ou si tu veux la gèrer de manière explicite en utilisant la fonction
originelle, tu défini la globale:

static boost::scoped_ptr<MyObject> my_object_handler(globale_ressource());

Puis
my_object_handler.reset(); //Pour une destruction explicite.

Sinon, l'objet sera détruit à la destruction de my_object_handler.


[...]
Nécessaire, c'et beaucoup dire. L'OS s'en occupe tout seul en général.

Les seuls objets qui posent éventuellement problème sont ceux qui
spawn des threads mais ceux là sont en général explicitement gérés
(ie. prévenus de la fermeture du framework).



Certes, mais si on compile en mode debug, on se prend des tas d'injures
disant que la bibliothèque ne sait pas restituer proprement la mémoire, et
ça gâche la qualité du produit.
Au minimum ça peut conduire à traquer des bugs qui sont en fait
"volontaires", ce qui est toujours désagréable.

Pour les cas où cela serait vraiment nécessaire, par exemple pour
rendre une ressource non géré par le système, tu pourrais utiliser un
smart pointer dans la static.



C'est ce que je voulais dire en parlant de lock-count.
Bon là on perd le bénéfice du test évité dans l'accesseur...


De toutes façon, en environement multithreadé, le lock count ne te
servira à rien. Il suffit d'un seul tread qui se termine de façon
anormale (ie. sans dépiler sa pile si tu utilise RAII) pour le compte
soit faussé.


Mais surtout c'est complètement remis en cause si l'utilisateur lui-même
gère mal ses cycles de vie. J'aimerais bien que ma bibliothèque soit un peu
blindée contre ça.


Peut être le plus simple est d'utiliser un smart_pointer dans la
fonction d'accès, si l'objet est déjà détruit, il renverra NULL et ton
destructor custom agira en conséquence.

Pour l'utilisateur, il suffit de mettre dans la doc que tu n'assure pas
l'utilisation des zones de mémoire allouées par ton objet après la
sortie de main() mais que rendre la zone mémoire (delete) est robuste
ensuite, à lui de gèrer ses destructeurs pour qu'ils n'utilisent pas
cette mémoire.

Michael



Patrick 'Zener' Brunet
Le #313567
Bonjour.

"Michael DOUBEZ" 476248f0$0$2213$
"Michael DOUBEZ" le message de news:
476199a3$0$24073$
[...]
Et alors, de manière aléatoire selon l'ordre d'assemblage des
modules compilés, les objets de l'utilisateur sont construits avant
le framework et donc l'utilisent à l'état non encore construit !!!
Et inversement pour la destruction finale :-[
La solution est un idiome courant de C++. L'instantiation des

globales n'est pas spécifiée mais l'intanciation des statiques de
fonction oui:
au premier apel de la fonction. Donc il suffit d'utiliser des
fonctions à la place des globales.

MyObject* globale_ressource()
{
static MyObject* my_object=new MyObjext(1,2,3,4);

return my_object;
}
[...]


Mais par contre, pas de destruction propre possible avec ça...


Je ne vois pas en quoi cela enpèche la destruction de l'objet -
avec un smart_pointer par exemple:

MyObject* globale_ressource()
{
static boost::scoped_ptr<MyObject>
my_object(new MyObjext(1,2,3,4));

return my_object.get();
}

Ou si tu veux la gèrer de manière explicite en utilisant la fonction
originelle, tu défini la globale:

static boost::scoped_ptr<MyObject>
my_object_handler(globale_ressource());

Puis
my_object_handler.reset(); //Pour une destruction explicite.

Sinon, l'objet sera détruit à la destruction de my_object_handler.



Dans le second cas c'est plus évident, puisque le pointeur est global. Dans
le second cas (static interne à une fonction) je ne sais pas à quel moment
le pointeur est détruit, mais ça doit être pareil.
Donc c'est lié à sa position dans le code, et ça peut être trop tôt dans le
cas qui nous préoccupe, comme lorsque je voulais utiliser un autre type
d'objet manager global.
Si l'utilisateur a déclaré un objet client global et que son code est
assemblé au-dessus du scoped_ptr, la destruction se faisant dans l'ordre
remontant, le client aura affaire à un pointeur désalloué :-(

Alors en fait il n'a jamais affaire *consciemment* à ce pointeur, puisque
c'est un inside de mon framework.
Et donc je peux en fait envisager de coder un comportement de repli dans le
framework, qui servira en phase de destruction, lorsque le framework a été
détruit et pas encore les clients.

=> Je crois que la bonne solution est là :-D

[...]
Nécessaire, c'et beaucoup dire. L'OS s'en occupe tout seul

en général.
Les seuls objets qui posent éventuellement problème sont
ceux qui spawn des threads mais ceux là sont en général
explicitement gérés (ie. prévenus de la fermeture du framework).


Certes, mais si on compile en mode debug, on se prend des tas
d'injures disant que la bibliothèque ne sait pas restituer proprement
la mémoire, et ça gâche la qualité du produit.
Au minimum ça peut conduire à traquer des bugs qui sont en fait
"volontaires", ce qui est toujours désagréable.

Pour les cas où cela serait vraiment nécessaire, par exemple
pour rendre une ressource non géré par le système, tu pourrais
utiliser un smart pointer dans la static.



C'est ce que je voulais dire en parlant de lock-count.
Bon là on perd le bénéfice du test évité dans l'accesseur...


De toutes façon, en environement multithreadé, le lock count ne te
servira à rien. Il suffit d'un seul tread qui se termine de façon
anormale (ie. sans dépiler sa pile si tu utilise RAII) pour le compte
soit faussé.



Voir ma réponse à James pour ça:
Pour moi un thread secondaire est une ressource allouée par le thread
principal, et ça justifie un cleanup complet en sortie comme pour toute
ressouce du thread principal. Ne pas le faire est une faute (je suis très
dur envers moi-même).
Donc chaque thread secondaire est supposé faire son ménage intime, et le
thread principal est supposé vérifier l'arrêt de tous les autres. S'il est
forcé d'avorter salement un thread, il signale une anomalie...
S'il y a des technos qui permettent de faire autrement, navré de jouer au
puriste mais je n'en veux pas, ou je devrai les hacker pour arriver à ça :-)

Donc ce qui m'intéresse pour valider ma solution, c'est de sortir avec 0
faute dans le cas où ces règles sont respectées. C'est à dire que je veux
qu'elles le soient même si l'utilisateur déclare naïvement un objet global
ou toute autre construction qui respecte les règles du C++.

Peut être le plus simple est d'utiliser un smart_pointer dans la
fonction d'accès, si l'objet est déjà détruit, il renverra NULL
et ton destructor custom agira en conséquence.



Oui, mais il n'est pas rare de voir un code invoquer un objet pour faire une
opération ultime avant de le détruire, s'il sait pertinemment que cet objet
a bien été construit, il ne reteste pas la validité du pointeur.
Donc le coup du pointeur qui se vide tout seul dans vos pattes, c'est un peu
casse-gueule, à moins de documenter de l'inside, ce qui toujours contraire à
l'objectif.

Pour l'utilisateur, il suffit de mettre dans la doc que tu n'assure pas
l'utilisation des zones de mémoire allouées par ton objet après la
sortie de main() mais que rendre la zone mémoire (delete) est
robuste ensuite, à lui de gèrer ses destructeurs pour qu'ils
n'utilisent pas cette mémoire.



Je crois que la bonne solution est apparue un peu plus haut. Elle est
composite:
- utiliser un objet global ou autre scoped_ptr pour assurer la destruction
de l'objet de service,
- modifier le framework pour qu'il soit capable de gérer cette situation
durant la phase de destruction des objets clients résiduels.

Ainsi l'utilisateur n'y verra que du feu.

Je vais étudier ça (c'est de la chirurgie crânienne mais j'y arriverai).
Merci beaucoup pour ce brainstorming.

--
Cordialement.
--
/**************************************************
* Patrick BRUNET
* E-mail: lien sur http://zener131.free.fr/ContactMe
**************************************************/




Patrick 'Zener' Brunet
Le #313566
Grrr! faute de frappe:

"Patrick 'Zener' Brunet" un goret manchot dans le message de news:
fk0efe$sgt$

Lire en fait:

Dans le second cas c'est plus évident, puisque le pointeur est global.
Dans le **premier** cas (static interne à une fonction)
je ne sais pas à quel moment le pointeur est détruit, mais ça doit être
pareil.
...

Sorry.
Publicité
Poster une réponse
Anonyme