1

Puisque j'ai pris du temps pour en parler dans le topic du presse papier je vais mettre ça à un endroit plus adapté smile
Donc le problème bien connu quand on fait du C++ c'est la gestion de mémoire et en particulier le fait que chacun fait un peu à sa manière. Un truc que j'aime bien, pour ceux qui connaissent pas, c'est la gestion de mémoire d'objective-C et je voulais essayer de reproduire ça pour un projet C++, histoire de voir.
Donc le principe est simple, chaque objet a un compteur de références, quand vous voulez la "propriété" de l'objet, vous l'incrémentez (retain), sinon vous le décrémentez (release). Par défaut tout objet que vous recevez (valeur de retour ou paramètre) n'est pas "à vous", ça signifie que vous pouvez l'utiliser le temps de votre fonction, mais vous ne pouvez pas vous attendre à ce qu'il continue d'exister car quelqu'un d'autre en est propriétaire. Si vous voulez le garder pour plus tard, il faut le "retenir".
class StringPrinter {
    char *str;
public:
    C(char *param) {
        str = retain(param);
    }
    ~C() {
        release(str);
    }
    void print() {
        printf("%s\n", str);
    }
};

Alors que pour les cas de base :
void function(char *param) {
    /* rien à faire ici, la string n'est pas à nous mais on peut s'en servir */
    printf("%s", param);
}

Bien sûr, il faut éventuellement faire gaffe à libérer ce qu'il y avait avant.
static char *keptStr;
void setStringToPrint(char *param) {
    release(keptStr);         /* release previous */
    keptStr = retain(param);  /* keep this one current */
}
void print() {
    printf("%s", keptStr);
}

Tout ça marche merveilleusement bien pour les paramètres, mais pour les valeurs de retour c'est plus compliqué : si une fonction crée un objet juste pour la retourner elle en est la seule propriétaire. Lorsqu'elle se termine, elle doit donc logiquement le libérer. Problème alors, il n'appartient plus à personne et va être détruit. Pour pallier à ça on l'inscrit dans un pool d'autorelease, qui garde une liste d'objets qui n'appartiennent plus à personne mais laisse une chance avant de les détruire. Il est donc possible pour les appelants d'utiliser l'objet jusqu'à ce qu'on décide de supprimer ce qui est dans le pool (ou de le retenir s'ils souhaient le garder pour plus tard). On peut donc écrire ceci :
StringPrinter *getMyName() {
    return autorelease(new StringPrinter("Brunni"));
}
void function() {
    StringPrinter *name = getMyName();
    /* name n'appartient à personne mais existe toujours. On peut le retenir au besoin */
    name->print();
}
void main() {
    function();
    /* détruit ce qui ne sert vraiment plus à personne */
    drain_autorelease_pool();
}

Enfin, l'endroit typique pour "drainer" un pool est à chaque étape d'une boucle d'un jeu, après la gestion d'un événement de l'UI, etc. ainsi les valeurs de retour sont valables durant toute une hiérarchie d'appels réalisant une tâche, puis seront détruits si personne n'a décidé de les garder pour plus tard.
L'avantage de cette façon de gérer la mémoire est qu'elle est rapide (pas de GC) et très simple une fois qu'on l'a intégrée. Ca devient un automatisme, à tel point d'ailleurs qu'en Objective-C actuel on n'est plus obligé d'écrire ça, le compilo le fait pour nous et on écrit l'équivalent de ce qu'on ferait en java.

Donc voilà, je me suis amusé à reproduire ça en C++, d'abord en proposant une classe de base. C'est trivial, mais ça peut toujours aider comme base.// Fichier .h #include <cassert> class Object { int retainCount; int pendingReleases; public: Object() : retainCount(1), pendingReleases(0) {} virtual ~Object() {} void _retain() { retainCount++; } void _autorelease(); void _release() { if (--retainCount == 0) { // Mismatch (release called too many times) assert(!pendingReleases); delete this; } } static void drain_autorelease_pool(); }; template <class T> static inline T *retain(T *obj) { obj->_retain(); return (T*) obj; } template <class T> static inline T *autorelease(T *obj) { obj->_autorelease(); return (T*) obj; } static inline void release(Object *obj) { obj->_release(); } static inline void drain_autorelease_pool() { Object::drain_autorelease_pool(); }// Fichier .cpp #include <stdlib.h> #include <stdio.h> #include "objectruntime.h" static const unsigned initialPoolSize = 1024, poolSizeIncrement = 1024; static Object **pool = new Object*[initialPoolSize]; static unsigned poolCount, currentPoolSize = initialPoolSize; void Object::_autorelease() { // Not yet in pool => put it if (!pendingReleases) { // Need more elements for the pool? if (poolCount >= currentPoolSize) { currentPoolSize += poolSizeIncrement; pool = (Object**) realloc(pool, currentPoolSize * sizeof(Object**)); } // Store into the pool pool[poolCount++] = this; pendingReleases = 1; } else pendingReleases++; } void Object::drain_autorelease_pool() { Object **poolPtr = pool; printf("Draining pool: %d objects\n", poolCount); while (poolCount--) { Object *objToRelease = *poolPtr++; // Does not destroy it if (objToRelease->pendingReleases < objToRelease->retainCount) objToRelease->retainCount -= objToRelease->pendingReleases; else { // Mismatch (release called too many times) assert(objToRelease->pendingReleases == objToRelease->retainCount); delete objToRelease; } } }
Les fonctions en template sont là pour simplifier, on peut alors appeler MonObjet *obj = retain(new MonObjet()); par exemple. On peut aussi proposer les primitives retain, release etc. comme méthodes de la classe (il vous faut juste une classe qui étende Object et prenne le véritable type en template arg). Exemple :
class TestObject: public Object {
public:
	TestObject() { printf("Constructed!\n"); }
	~TestObject() { printf("Destroyed!\n"); }
	int getValue() { return 10; }
};
TestObject *createTestObject() {
	TestObject *retval = new TestObject();
	return autorelease(retval);
}
void useTestObject(TestObject *obj) {
	printf("Value: %d\n", obj->getValue());
}
int main(int argc, char *argv[]) {
	TestObject *ret = createTestObject();
	useTestObject(ret);
	drain_autorelease_pool();
	return 0;
}



Maintenant le challenge smile c'est de pouvoir faire ça sur n'importe quel objet, même en C pur (si vous rêviez de pouvoir retourner un char* et ne pas avoir à le désallouer par exemple). On peut faire ça assez simplement en fin de compte et avec un impact relativement faible.
Donc mon idée de base c'est d'avoir une table de correspondance entre l'adresse d'un objet et son retain count. Evidemment, on ne va pas faire une liste adresse -> retain count, qui serait longue à parcourir. Une table de hachage est plus efficace, et c'est assez facile à faire en extrayant quelques bits d'adresse pour l'indexer. Typiquement un objet @ 0x12345678 on peut imaginer extraire les avant-derniers bits (0x567) et utiliser ça comme index. Il suffit alors de voir s'il y a un objet qui correspond dans la table et si son adresse est exactement la même. Ainsi pour chaque entrée de la table on a une ou plusieurs entrées (liste chaînée). Dans mon cas j'ai choisi 4096 entrées, soit les bits 11 à 2 pour le hachage (32 bits, il faudrait prendre 12 à 3 en 64 bits).
Ainsi un retain incrémente le refcount et un release le décrémente. S'il tombe à zéro lors d'un release alors l'objet peut être supprimé. Pour l'autorelease pareil, mais il faut aussi associer au ref count un compteur "d'autoreleases en attente", et au moment de drainer le pool on va décrémenter le refcount de cette valeur et effacer l'objet s'il tombe à zéro (= plus personne ne s'en sert).
Cependant dans le cas de l'autorelease, la suppression est un peu complexe en C++ car il n'y a pas de pointeur vers le destructeur et ce dernier n'est pas garanti d'exister. Et on peut vouloir fonctionner en C pur aussi. Il faut alors fournir une façon d'effacer les objets qui puisse être invoquée de façon différée. Dans mon implém, j'ai créé une fonction avec template qui appelle delete, ainsi une version de cette fonction sera automatiquement créée pour chaque type d'objet qu'on inscrit au pool d'autorelease (il s'agit de 3 instructions, ce n'est pas grand chose). On peut aussi imaginer laisser au programmeur le soin fournir sa fonction, et dans le cas du C il pourrait passer systématiquement "free".

Voilà, maintenant le code pour ceux à qui ça peut servir : smile// unmanagedobjectruntime.h #ifndef UNMANAGEDOBJECTRUNTIME_H #define UNMANAGEDOBJECTRUNTIME_H // Private extern void _unmanaged_put_in_pool(void *obj); extern void _unmanaged_retain(void *obj); extern bool _unmanaged_release(void *obj); extern void _unmanaged_autorelease(void *obj, void (*deleter)(void*)); template <class T> static void _unmanaged_deleter(void *ptr) { delete static_cast<T*>(ptr); } // Public extern void unmanaged_autorelease(void *obj); extern void unmanaged_pool_drain(); template <class T> static inline T *unmanaged_put_in_pool(T *obj) { _unmanaged_put_in_pool(obj); return obj; } template <class T> static inline T *unmanaged_retain(T *obj) { _unmanaged_retain(obj); return obj; } template <class T> static inline T *unmanaged_autorelease(T *obj) { _unmanaged_autorelease(obj, &_unmanaged_deleter<T>); return obj; } template <class T> static inline void unmanaged_release(T *obj) { if (_unmanaged_release(obj)) delete obj; } #endif#include <cassert> #include <stdint.h> #include "unmanagedobjectruntime.h" // Power of two - 1 #define HASH_BIT_MASK 4095 // Number of LSB useless in an address (you may want to change to 3 for 64 bits...) #define REQUIRED_ADDY_SHIFT 2 struct RetainedObj { void *realAddress; void (*deleter)(void*); RetainedObj *nextIfAny; unsigned short retainCount, pendingReleases; RetainedObj() : realAddress(0), nextIfAny(0), retainCount(0), pendingReleases(0) {} RetainedObj(void *object) : realAddress(object), nextIfAny(0), retainCount(1), pendingReleases(0) {} }; static RetainedObj retainedObjList[HASH_BIT_MASK + 1]; void _unmanaged_put_in_pool(void *object) { unsigned hash = ((uintptr_t)object >> REQUIRED_ADDY_SHIFT) & HASH_BIT_MASK; RetainedObj *line = retainedObjList + hash, *lastLine = line; // Make sure it doesn't exist yet assert(line->realAddress != object); while (lastLine->nextIfAny) { assert(lastLine->nextIfAny->realAddress != object); lastLine = lastLine->nextIfAny; } // Else, put in db. 1) room directly in line? if (!line->realAddress) { line->realAddress = object; line->retainCount = 1; } else { // 2) Line already exist, append to linked list lastLine->nextIfAny = new RetainedObj(object); } } void _unmanaged_retain(void *object) { unsigned hash = ((uintptr_t)object >> REQUIRED_ADDY_SHIFT) & HASH_BIT_MASK; RetainedObj *line = retainedObjList + hash; // Find if already in db, and increment retain count in such case if (line->realAddress == object) { line->retainCount++; return; } // May be in linked list if (line->nextIfAny) { do { line = line->nextIfAny; if (line->realAddress == object) { line->retainCount++; return; } } while (line->nextIfAny); } // Shouldn't come here (should put in pool first) assert(0); } bool _unmanaged_release(void *object) { // Find in db unsigned hash = ((uintptr_t)object >> REQUIRED_ADDY_SHIFT) & HASH_BIT_MASK; RetainedObj *line = retainedObjList + hash; // 1) the address of the object is in the base of the line if (line->realAddress == object) { if (--line->retainCount == 0) { // Remove the line and ask to remove the actual object line->realAddress = 0; return true; } return false; } // 2) the address is in the linked list RetainedObj *next = line->nextIfAny; while (next) { if (next->realAddress == object) { // Found object, remove from line if necessary if (--next->retainCount == 0) { // Remove from linked list line->nextIfAny = next->nextIfAny; delete next; return true; } return false; } line = next; next = line->nextIfAny; } // Shouldn't come here (not found in list) assert(0); return false; } void _unmanaged_autorelease(void *object, void (*deleter)(void*)) { unsigned hash = ((uintptr_t)object >> REQUIRED_ADDY_SHIFT) & HASH_BIT_MASK; RetainedObj *line = retainedObjList + hash; // Find if already in db, and increment retain count in such case if (line->realAddress == object) { line->deleter = deleter; line->pendingReleases++; return; } // May be in linked list if (line->nextIfAny) { do { line = line->nextIfAny; if (line->realAddress == object) { line->deleter = deleter; line->pendingReleases++; return; } } while (line->nextIfAny); } // Shouldn't come here (should put in pool first) assert(0); } void unmanaged_pool_drain() { // Browse the list and delete objects where applicable for (unsigned i = 0; i < HASH_BIT_MASK + 1; i++) { RetainedObj *line = retainedObjList + i; if (line->realAddress) { assert(line->retainCount >= line->pendingReleases); line->retainCount -= line->pendingReleases; // Time to delete the object (use the deleter func) if (line->retainCount == 0) { assert(line->deleter && line->pendingReleases > 0); line->deleter(line->realAddress); } } // Same for the linked list RetainedObj *next = line->nextIfAny; while (next) { assert(next->retainCount >= next->pendingReleases); next->retainCount -= next->pendingReleases; if (next->retainCount == 0) { // Delete item and remove from linked list assert(next->deleter && next->pendingReleases > 0); next->deleter(next->realAddress); line->nextIfAny = next->nextIfAny; delete next; } line = next; next = line->nextIfAny; } } }

L'utilisation se fait alors comme ceci :
class TestObject {
public:
	TestObject() { printf("Constructed!\n"); }
	~TestObject() { printf("Destroyed!\n"); }
	int getValue() { return 10; }
};
class MyApp {
	TestObject *myObject;
public:
	MyApp(TestObject *obj) {
		myObject = unmanaged_retain(obj);
	}
	void testMethod() {
		printf("Value: %d\n", myObject->getValue());
	}
	~MyApp() {
		unmanaged_release(myObject);
	}
};
TestObject *getTestObject() {
	TestObject *retval = unmanaged_put_in_pool(new TestObject());
	return unmanaged_autorelease(retval);
}
int main(int argc, char** argv) {
	MyApp *app = new MyApp(getTestObject());
	app->testMethod();
	delete app;
	unmanaged_pool_drain();
	return 0;
}

On pourrait imaginer mettre automatiquement les objets dans le pool au premier autorelease ou retain, mais dans l'implém j'ai rendu l'appel à unmanaged_put_in_pool obligatoire pour commencer à gérer un objet non managé.
avatar
Highway Runners, mon jeu de racing à la Outrun qu'il est sorti le 14 décembre 2016 ! N'hésitez pas à me soutenir :)

https://itunes.apple.com/us/app/highway-runners/id964932741

2

(pas de solution pour l'instant. Tu peux essayer le java par contre embarrassed)

3

Erreur: la réponse est erronée
avatar
Proud to be CAKE©®™


GCC4TI importe qui a problème en Autriche, pour l'UE plus et une encore de correspours nucléaire, ce n'est pas ytre d'instérier. L'état très même contraire, toujours reconstruire un pouvoir une choyer d'aucrée de compris le plus mite de genre, ce n'est pas moins)
Stalin est l'élection de la langie.

4

Tu as raison de réserver le topic, on risquait de te piquer le nom ! tongue

(PS : La gestion mémoire par comptage de références ça pue ^^)
avatar
Le scénario de notre univers a été rédigée par un bataillon de singes savants. Tout s'explique enfin.
T'as un problème ? Tu veux un bonbon ?
[CrystalMPQ] C# MPQ Library/Tools - [CrystalBoy] C# GB Emulator - [Monoxide] C# OSX library - M68k Opcodes

5

#snif#
avatar
Highway Runners, mon jeu de racing à la Outrun qu'il est sorti le 14 décembre 2016 ! N'hésitez pas à me soutenir :)

https://itunes.apple.com/us/app/highway-runners/id964932741

6

Brunni (./1) :
Tout ça marche merveilleusement bien pour les paramètres, mais pour les valeurs de retour c'est plus compliqué : si une fonction crée un objet juste pour la retourner elle en est la seule propriétaire. Lorsqu'elle se termine, elle doit donc logiquement le libérer.

Je comprends pas pourquoi elle devrait le libérer en fait. Elle crée l'objet avec un refcount de 1, ça semble normal. Puis elle le retourne,
Brunni (./1) :
Problème alors, il n'appartient plus à personne et va être détruit.

Ben pourquoi l'objet n'appartient pas à la fonction qui a appelé la fonction créatrice ? Le refcount est à un, l'objet lui a été donné par retour, donc tout va bien, on sais où est l'objet et à qui il appartient, et la fonction appelante est la seule à savoir quand le-dit objet devra être libéré, puisque c'est elle qui s'en sert.
Ya un truc compliqué que j'ai pas dû comprendre, parce que ça me parait pas plus dur que ça en fait grin

7

Si tu fais comme ça, le code devient moins agréable à écrire et déroge à l'automatisme qui veut qu'un objet que tu n'as pas explicitement créé (avec new) ne t'appartient pas, donc que tu n'as pas à t'en occuper.
Par exemple avec ma solution je peux écrire ceci :
char *sayMyAge(int years) {
    char *result = new char[256];
    sprintf(result, "Wow, you are %d years old!");
    return autorelease(result);
}
void caller() {
    printf("%s\n", sayMyAge(42));
}

Alors qu'avec ta solution tu devras te préoccuper de la valeur de retour. C'est ennuyeux si elle t'est juste donnée à titre indicatif ou qu'elle ne t'intéresse pas*, et c'est un problème en cas de refactoring où tu as modifié une fonction retournant une primitive pour retourner cette fois un objet : il faut faire très attention partout où tu l'utilisais.
char *sayMyAge(int years) {
    char *result = new char[256];
    sprintf(result, "Wow, you are %d years old!");
    return result;
}
void caller() {
    char *temp = sayMyAge(42);
    printf("%s\n", temp);
    release(temp);
}

* Exemple d'API ou un même objet est retourné sans forcément une utilité réelle :
Image *image = new Image("m68k.png");
image->scale(2, 2)->rotate(30)->draw(0, 0);

Mais si tu évites ces constructions, c'est un compromis envisageable. On redéfinit la sémantique comme "tu es propriétaire de tout objet créé ou retourné, et pas le reste".
avatar
Highway Runners, mon jeu de racing à la Outrun qu'il est sorti le 14 décembre 2016 ! N'hésitez pas à me soutenir :)

https://itunes.apple.com/us/app/highway-runners/id964932741

8

Heu... je passe probablement à côté de quelque chose, mais c'est une syntaxe (et une implémentation) quand même particulièrement lourde pour tenter de s'affranchir de quelques paires de new/delete, non ? Si tu as besoin de ça sans arrêt mieux vaut directement utiliser un langage avec GC qui s'en chargera de façon plus efficace (et sans avoir à ajouter des appels de fonctions magiques un peu partout), mais si tu restes en C++ il y a de grandes chances que tu ne veuilles pas vraiment utiliser ce genre d'usine à gaz si souvent que ça, par exemple parce la question de la mémoire est souvent assez locale et que tu n'as pas besoin de l'enfouir sous une couche de magie-qui-libère-toute-seule.

En mettant de côté l'overhead qui est une discussion à part entière, j'ai du mal à voir ce que ta solution apporte par rapport au header <memory> du C++ par exemple ?
avatar
All right. Keep doing whatever it is you think you're doing.
------------------------------------------
Besoin d'aide sur le site ? Essayez par ici :)

9

En pratique ce petit plus simplifie bien la vie (enfin je parle d'Objective-C, j'ai encore jamais essayé en C++ avec cette surcouche justement).
A voir comment ça s'intègre avec ce que j'ai déjà, mais en général si tu utilises des malloc/delete c'est pour faire des éléments partagés, dynamiques, etc. et dans le cadre du traitement de l'UI ou d'un jeu, ça aide beaucoup de ne pas avoir à réfléchir quand tu peux libérer tes objets. Avec ce mécanisme simple, tu ne risques pas de te tromper tant que tu le suis (= si j'ai fait un new je release quand j'ai plus besoin).
Le header memory du C++ est plus complexe à utiliser et prone aux erreurs, alors qu'ici on n'a que 2 concepts à apprendre.
Et justement j'ai mesuré un overhead négligeable (malloc/free sont infiniment plus gourmands en comparaison de ça), contrairement à un GC. Et la question du GC ne se pose pas parce qu'on part du postulat que le projet doit être en C++ (jeu typiquement). Et quand tu dis "lourd", ne confond pas la première implémentation (triviale) avec la seconde, qui n'est intéressante que dans des cas très particuliers.
avatar
Highway Runners, mon jeu de racing à la Outrun qu'il est sorti le 14 décembre 2016 ! N'hésitez pas à me soutenir :)

https://itunes.apple.com/us/app/highway-runners/id964932741

10

C'est le genre de choses qui s'implémente assez souvent pour un usage bien spécifique, mais l'utiliser absolument partout, je reste assez sceptique. Je serais curieux de voir un bench si tu fais un projet avec ça. Par contre comme ton implémentation n'est pas thread-safe ça va soit imposer un usage très limité, soit te demander des modifications importantes, soit avoir des performances assez catastrophiques. Dans tous les cas je parie sur le GC, personnellement smile
avatar
All right. Keep doing whatever it is you think you're doing.
------------------------------------------
Besoin d'aide sur le site ? Essayez par ici :)

11

Je vais pas installer un GC pour iOS ou Android pour coder en C++. Le GC est bien sûr plus adapté pour ce genre de trucs intensifs, maintenant en Java on sait ce que ça fait, des drops de framerate à foison. Donc des fois ça peut valoir la peine de considérer une solution intermédiaire, et dans ce sens j'aime bien la gestion de mémoire déterministe. wink
Après ouais "partout" c'est exagéré, dans mon cas j'utilise ça partout où je manipule des références (et c'est pas si souvent, mais c'est justement des parties merdiques...).
Mais comme je l'ai dit, j'attends de voir comment ça se mixe avec le tout, en général il vaut mieux éviter l'allocation dynamique si on peut. Mais je préfère à priori cette solution au refcounting dégueulasse + copy-constructor qu'on trouve classiquement en C++, donc je voulais la partager.

Ce n'est pas thread safe non, je voulais pas inclure ça à cette solution. Mais j'avais réfléchi de tête et ça ne semblait pas causer de souci (l'incrémentation et la décrémentation devraient être atomiques mais c'est tout, le reste est bon), il n'y a que pour l'autorelease qu'il faut faire gaffe, et c'est plus rare d'utiliser ça, et là encore on peut faire une version assez efficace (avec le besoin d'un lock que si la taille du pool est dépassée).
avatar
Highway Runners, mon jeu de racing à la Outrun qu'il est sorti le 14 décembre 2016 ! N'hésitez pas à me soutenir :)

https://itunes.apple.com/us/app/highway-runners/id964932741

12

Et pourquoi pas utiliser la solution de Qt (pour les objets de données), à savoir le comptage de références implicit? Ça évite de devoir appeler retain et release tout le temps et ça contourne entièrement la problématique d'autorelease. (Et d'ailleurs, l'implémentation de Qt est aussi thread-safe.)

Quant à l'histoire de gérer des compteurs de références par une table de hachage, j'ai essayé ça une fois (avec une QHash) et ça ramait à fond, le profile pointant du doigt la table de hachage. J'ai mis les compteurs directement dans les objets en question (ce que je n'ai pas voulu faire au début parce que les objets en question sont const et donc les compteurs sont mutable, mais c'est une longue histoire) et ça a accéléré nettement tout le programme. Du coup, je ne peux pas conseiller cette technique.
avatar
Mes news pour calculatrices TI: Ti-Gen
Mes projets PC pour calculatrices TI: TIGCC, CalcForge (CalcForgeLP, Emu-TIGCC)
Mes chans IRC: #tigcc et #inspired sur irc.freequest.net (UTF-8)

Liberté, Égalité, Fraternité

13

Kevin Kofler (./12) :
J'ai mis les compteurs directement dans les objets en question
(Du coup, tous tes objets foutent en l'air 4 à 8 octets de mémoire juste pour le plaisir smile)
avatar
Le scénario de notre univers a été rédigée par un bataillon de singes savants. Tout s'explique enfin.
T'as un problème ? Tu veux un bonbon ?
[CrystalMPQ] C# MPQ Library/Tools - [CrystalBoy] C# GB Emulator - [Monoxide] C# OSX library - M68k Opcodes

14

Oué enfin quand tu fait de l'objet, en plus avec de telle fonctionalitée, tu n'est pas a quelques octets prets.
avatar
Proud to be CAKE©®™


GCC4TI importe qui a problème en Autriche, pour l'UE plus et une encore de correspours nucléaire, ce n'est pas ytre d'instérier. L'état très même contraire, toujours reconstruire un pouvoir une choyer d'aucrée de compris le plus mite de genre, ce n'est pas moins)
Stalin est l'élection de la langie.

15

GoldenCrystal (./13) :
Kevin Kofler (./12) :
J'ai mis les compteurs directement dans les objets en question
(Du coup, tous tes objets foutent en l'air 4 à 8 octets de mémoire juste pour le plaisir smile)

Dans le contexte en question, ce n'est pas un problème (et la table de hachage prenait aussi de la place).
avatar
Mes news pour calculatrices TI: Ti-Gen
Mes projets PC pour calculatrices TI: TIGCC, CalcForge (CalcForgeLP, Emu-TIGCC)
Mes chans IRC: #tigcc et #inspired sur irc.freequest.net (UTF-8)

Liberté, Égalité, Fraternité

16

Kevin Kofler (./12) :
Et pourquoi pas utiliser la solution de Qt (pour les objets de données), à savoir le comptage de références implicit? Ça évite de devoir appeler retain et release tout le temps et ça contourne entièrement la problématique d'autorelease. (Et d'ailleurs, l'implémentation de Qt est aussi thread-safe.)

Je me rappelle que c'était très bien fichu à ce niveau-là quand je m'étais amusé avec Qt à l'époque, mais mes souvenirs sont vagues. C'était pour les trucs de données seulement non ? (avec le copy-on-write tout ça)
Tu as un exemple d'objet simple créé soi-même avec la possibilité d'en passer des références à gauche et à droite ? smile

./6> En fait je pense que la meilleure façon si tu ne veux pas de "pool", ce serait de ne jamais retourner une référence vers un objet, ou que si tu le fais c'est qu'elle t'appartient encore juste à la sortie (plus tard on ne sait pas, d'où l'intérêt de l'appelant de faire un retain s'il compte le garder).
Typiquement on pourrait tout à fait imaginer qu'une méthode retourne une référence vers l'objet lui-même (return this) ou une référence vers un objet qu'elle possède, mais PAS retourner un objet nouvellement créé et qui ne sert qu'à l'appelant. Dans ce cas-là il faudrait passer un pointeur en argument.
class Number: public Object {
    private int nb;
public:
    Number() : nb(0) {}
    Number *setValue(int val) {
        nb = val;
        return this;
    }
};

Tandis que:
void createNumber(Number **dest, int value) {
    /* Devra être libéré par l'appelant */
    *dest = new Number()->setValue(value);
}

Je ne me suis pas encore bien décidé si je préfère ça ou utiliser un truc genre shared_ptr (boost). L'avantage de cette méthode c'est quand même la transparence (hormis pour les deux primitives), tu utilises ton propre objet partout.
avatar
Highway Runners, mon jeu de racing à la Outrun qu'il est sorti le 14 décembre 2016 ! N'hésitez pas à me soutenir :)

https://itunes.apple.com/us/app/highway-runners/id964932741

17

Brunni (./16) :
Je me rappelle que c'était très bien fichu à ce niveau-là quand je m'étais amusé avec Qt à l'époque, mais mes souvenirs sont vagues. C'était pour les trucs de données seulement non ? (avec le copy-on-write tout ça)

Effectivement. Les QObjects utilisent un autre modèle (hiérarchie en arbre, libérer le parent libère automatiquement les fils).
Tu as un exemple d'objet simple créé soi-même avec la possibilité d'en passer des références à gauche et à droite ? smile

http://www.tigen.org/kevin.kofler/fmathl/dyngenpar/doc/dyngenpar_8h_source.html#l00678
QSharedDataPointer et QSharedData font presque tout pour toi.

Seul truc "magique" ici:
http://www.tigen.org/kevin.kofler/fmathl/dyngenpar/doc/dyngenpar_8h_source.html#l01288
(pour cloner correctement un AbstractLexerStateData à travers une méthode virtuelle qui se charge de cloner l'objet dérivé, pas seulement la partie abstraite). Ce n'est pas nécessaire dans les cas simples où ta classe de données est une classe concrète (enfin plus précisément, où c'est une classe "finale", c'est-à-dire que tu n'en dérives pas, contrainte que le langage C++ te laisse le soin de respecter toi-même).
avatar
Mes news pour calculatrices TI: Ti-Gen
Mes projets PC pour calculatrices TI: TIGCC, CalcForge (CalcForgeLP, Emu-TIGCC)
Mes chans IRC: #tigcc et #inspired sur irc.freequest.net (UTF-8)

Liberté, Égalité, Fraternité

18

Hmm ok je vois (pour la première partie en tous cas). Ca correspond probablement à ce que je m'étais amusé à implémenter :template<class T> class ref { T *ptr; unsigned *counter; void incref() { if (counter) (*counter)++; } void decref() { if (counter && --(*counter) == 0) delete ptr, delete counter; } public: ref() : ptr(0), counter(0) {} ref(const ref<T> &ref) : ptr(ref.ptr), counter(ref.counter) { incref(); } ref(T *ptr) : ptr(ptr) { counter = ptr ? new unsigned(1) : 0; } ~ref() { decref(); } ref<T> &operator = (const ref &ref) { // Using the same logic may free the original object if counter = 1 if (ref.ptr == ptr) return *this; // Else, do the equivalent of destructing "this" and constructing with "ref" decref(); ptr = ref.ptr, counter = ref.counter; incref(); return *this; } ref<T> &operator = (T *newPtr) { decref(); counter = (ptr = newPtr) ? new unsigned(1) : 0; return *this; } operator T* () const { return ptr; } T* operator -> () const { return ptr; } T& operator * () const { return *ptr; } bool operator == (const ref<T> &ref) const { return ref.ptr == ptr; } bool operator != (const ref<T> &ref) const { return ref.ptr != ptr; } };
Alors on n'a qu'à utiliser ref<Objet> à la place de Objet * pour une gestion automatique de la mémoire, et utiliser le modèle standard au besoin smile
class Image {
	const char *name;
public:
	Image(const char *name) : name(name) { cout << "Constructed " << name << endl; }
	~Image() { cout << "Destructed " << name << endl; }
	int getWidth() { return 42; }
};

ref<Image> g_image = new Image("1.png");

void method(Image *myImage) {
	cout << "Size is " << myImage->getWidth() << endl;
}

int main(int argc, const char * argv[]) {
	ref<Image> local;
	g_image = NULL; /* libération explicite */
	local = new Image("3.png"); /* affectation possible depuis un pointeur */
	method(local); /* passage à une fonction d'API n'utilisant pas ref<> */
	cout << "Locally: " << local->getWidth() << endl; /* Utilisation de ref<Image> comme une Image* */
	return 0;
}

png
=> Constructed 1.png
Destructed 1.png
Constructed 2.png
Size is 42
Locally: 42
Destructed 2.

Je pense que je vais choisir ça en fin de compte, ça me paraît assez facile à comprendre (juste dire aux débutants que s'ils veulent s'affranchir de la gestion de la mémoire ils n'ont qu'à utiliser ref<Truc> plutôt que Truc*) et comme ça marche pour tout sans polluer ceux qui ne veulent pas s'en servir, ça me paraît être un bon compromis. hehe
avatar
Highway Runners, mon jeu de racing à la Outrun qu'il est sorti le 14 décembre 2016 ! N'hésitez pas à me soutenir :)

https://itunes.apple.com/us/app/highway-runners/id964932741

19

Brunni (./16) :
d'où l'intérêt de l'appelant de faire un retain s'il compte le garder

argh, étant habitué à la programmation procédurale ou fonctionnelle, je trouve ça complètement horrible grin

20

La solution de Qt est bien documentée. smile
http://qt-project.org/doc/qt-4.8/qshareddata.html
http://qt-project.org/doc/qt-4.8/qshareddatapointer.html

Pour l'astuce du clone:
http://qt-project.org/doc/qt-4.8/qshareddatapointer.html#clone
Ça permet d'utiliser des classes abstraites ou autrement sous-classables dans un QSharedDataPointer.
avatar
Mes news pour calculatrices TI: Ti-Gen
Mes projets PC pour calculatrices TI: TIGCC, CalcForge (CalcForgeLP, Emu-TIGCC)
Mes chans IRC: #tigcc et #inspired sur irc.freequest.net (UTF-8)

Liberté, Égalité, Fraternité

21

Je conseille plutôt de passer par les pointeurs automatiques qui font parti de la nouvelle norme c++11.
De cette façon tu ne dépends pas d'une librairie externe.
http://www.cplusplus.com/reference/memory/shared_ptr/
http://www.cplusplus.com/reference/memory/weak_ptr/
http://www.cplusplus.com/reference/memory/unique_ptr/

Cela dit, ça ne ressemble plus vraiment au mécanisme du langage C# que tu décris (je n'ai jamais fait d'objective-C).
L'inconvénient de ce genre de pratique en c++, c'est que si as un bout de code externe (genre une lib) qui n'utilise que des pointeurs normaux, ça te bousille toute la jolie sémantique...

22

Euh, utiliser une feature de C++11, c'est bien en théorie mais pas une super idée en pratique tongue
(bonjour la portabilité...)
avatar
Zeroblog

« Tout homme porte sur l'épaule gauche un singe et, sur l'épaule droite, un perroquet. » — Jean Cocteau
« Moi je cherche plus de logique non plus. C'est surement pour cela que j'apprécie les Ataris, ils sont aussi logiques que moi ! » — GT Turbo

23

gon33 (./21) :
Je conseille plutôt de passer par les pointeurs automatiques qui font parti de la nouvelle norme c++11.De cette façon tu ne dépends pas d'une librairie externe.

L'implémentation de Qt est meilleure (comme partout – la STL sux!) et plus portable.

En particulier, les objets pointés par QSharedDataPointer doivent dériver de QSharedData qui prévoit un compteur de références dans l'objet pointé, donc on évite une allocation à part. (Il y a aussi QSharedPointer si on a besoin d'un pointeur intelligent sur quelque chose qui ne dérive pas de QSharedData. Le fonctionnement doit être à peu près le même que celui de std::shared_ptr.)

Et puis le partage est clairement implicit dans Qt alors que la STL t'oblige à utiliser explicitement un pointeur intelligent. C'est ce qui rend QtCore beaucoup plus agréable à utiliser que la STL.
avatar
Mes news pour calculatrices TI: Ti-Gen
Mes projets PC pour calculatrices TI: TIGCC, CalcForge (CalcForgeLP, Emu-TIGCC)
Mes chans IRC: #tigcc et #inspired sur irc.freequest.net (UTF-8)

Liberté, Égalité, Fraternité

24

gon33 (./21) :
L'inconvénient de ce genre de pratique en c++, c'est que si as un bout de code externe (genre une lib) qui n'utilise que des pointeurs normaux, ça te bousille toute la jolie sémantique...

Oui, c'est d'ailleurs le souci de ma solution. On ne peut pas passer des objets "normaux" à des fonctions attendant des références partagées, typiquement :
void fonction(ref<int> valeur) {
    printf("Valeur: %d", valeur);
}

void main() {
    int *ptrInt = new int(1);
    fonction(ptrInt);
    delete ptrInt;
}

Ca va planter car la référence créée automatiquement pour la fonction va prendre possession de l'objet, et l'effacera au retour de la fonction (ce qui est rarement ce qu'on veut, à moins d'avoir passé un new qqch comme paramètre). C'est pourquoi dans mon implém réelle les références créées automatiquement ne gèrent pas l'objet qui leur est assigné en réalité. Il faut déclarer explicitement que l'objet appartient à la référence.
void fonction(ref<int> valeur) {
    printf("Valeur: %d", valeur);
}

void main() {
    int *unmanagedPtr = new int(1);
    ref<int> managedPtr = make_ref(new int(1));
    fonction(unmanagedPtr);
    fonction(managedPtr);
    delete unmanagedPtr;
}
avatar
Highway Runners, mon jeu de racing à la Outrun qu'il est sorti le 14 décembre 2016 ! N'hésitez pas à me soutenir :)

https://itunes.apple.com/us/app/highway-runners/id964932741

25

Ya pas moyen de faire un simple wrapper C++ pour la fonction ?

26

C'est à dire ?
avatar
Highway Runners, mon jeu de racing à la Outrun qu'il est sorti le 14 décembre 2016 ! N'hésitez pas à me soutenir :)

https://itunes.apple.com/us/app/highway-runners/id964932741

27

que ce soit ton wrapper qui crée l'objet que va utiliser la fonction C/C++ qui n'a pas connaissance te ton mécanisme.

28

Euh, utiliser une feature de C++11, c'est bien en théorie mais pas une super idée en pratique tongue(bonjour la portabilité...)

J'en utilise tous les jours au taf, ça marche du tonerre :-)

Ok mais on est dans tous les cas coincés si on a une situation de ce type :

void maBiblio::displayScreen(Screen* s);

void main {
  ref<int> managedPtr = make_ref(new Screen());
  // je vais galérer à appeler displayScreen et conserver la structure du code.
}

29

Folco (./27) :
que ce soit ton wrapper qui crée l'objet que va utiliser la fonction C/C++ qui n'a pas connaissance te ton mécanisme.

Je ne suis toujours pas sûr de te comprendre mais si c'est le cas c'est déjà bon (cf. un exemple plus haut). C'est le cas contraire qui est embêtant, si une fonction utilise le mécanisme avec un objet qui n'en a pas conscience.
avatar
Highway Runners, mon jeu de racing à la Outrun qu'il est sorti le 14 décembre 2016 ! N'hésitez pas à me soutenir :)

https://itunes.apple.com/us/app/highway-runners/id964932741

30

gon33 (./29) :
J'en utilise tous les jours au taf, ça marche du tonerre :-)
J'espère que t'auras jamais à utiliser un autre compilo que GCC alors ^^
avatar
Zeroblog

« Tout homme porte sur l'épaule gauche un singe et, sur l'épaule droite, un perroquet. » — Jean Cocteau
« Moi je cherche plus de logique non plus. C'est surement pour cela que j'apprécie les Ataris, ils sont aussi logiques que moi ! » — GT Turbo