2009-10-15 14 views
1

J'ai une classe de base, Token. Il n'a pas d'implémentation et agit comme une interface de marqueur. C'est le type qui sera utilisé par les appelants.Héritage C++ pour les objets sur la pile

{ 
    Token t = startJob(jobId); 
    // ... (tasks) 
    // t falls out of scope, destructors are called 
} 

J'ai une classe dérivée, LockToken. Il s'enroule autour d'un mutex et assure que la serrure est acquise pendant la construction et libérée pendant la destruction. La méthode startJob est une méthode d'usine dans le sens où elle décide de renvoyer un Token (sans verrouillage) ou un LockToken (verrouillage).

Token startJob(int jobId) 
{ 
    return (jobId>0) ? LockToken() : Token() ; 
} 

Lorsque startJob renvoie une instance de base (un Token), tout fonctionne correctement. Dans l'autre cas (jobId> 0), il existe une copie de l'instance dérivée vers l'instance de base. Dans d'autres travaux, un Token différent est construit à partir du LockToken et le LockToken original tombe trop tôt hors de la portée, libérant le verrou dans la portée de startJob.

Comment puis-je m'en sortir? Puis-je changer startJob pour qu'il renvoie ou produise un Token vraiment covariant (ce qui signifie qu'il peut s'agir d'un LockToken)?

+1

Ceci est connu comme le problème de tranchage, si vous êtes curieux. http://stackoverflow.com/questions/274626/what-is-the-slicing-problem-in-c –

Répondre

9

Vous renvoyez un jeton par valeur. Cela signifie que vous ne renvoyez pas un LockToken, mais plutôt une copie Token construite à partir de votre instance LockToken.

Une approche beaucoup mieux serait d'utiliser boost :: shared_ptr. De cette façon, vos clients peuvent copier des choses sans avoir à s'inquiéter de la suppression. Quelque chose comme ceci:

#include <boost/shared_ptr.hpp> 

typedef boost::shared_ptr<void> Token; 

Token startJob(int jobId) 
{ 
    if (jobId < 1) return shared_ptr<void>(); 
    return shared_ptr<void>(new LockToken); 
} 

void use_it() 
{ 
    Token t = startJob(jobId); 
    // .... 
    // Destructors are called 
} 

Notez que vous ne avez plus besoin d'une Jeton de classe qui ne fait rien, et la classe locktoken est maintenant un détail de mise en œuvre qui est entièrement caché des clients, vous donnant possibilité de faire toutes sortes d'autres choses quand le jeton sort de sa portée.

+1

Non, shared_ptr . Il n'est même pas nécessaire d'avoir une classe appelée Token si elle ne fait rien. – janm

+0

Quoi qu'il en soit, vous devriez éditer le retour de la fonction pour qu'elle corresponde à ce qui est réellement retourné, ainsi que le type de 't' dans 'use_it' ... –

+1

@Matthieu: Voir le typedef en haut de l'exemple. Le point de la question est: "Comment puis-je retourner un jeton qui fait des choses différentes sur la destruction en fonction d'autres contraintes". Réponse: "fournissez un handle opaque et shared_ptr <> est un bon moyen de le faire." La fonction renvoie un shared_ptr ; il n'est pas nécessaire d'exposer le fait qu'il contient un pointeur sur un LockToken, c'est-à-dire un détail d'implémentation. Quelque chose comme: "return shared_ptr (static_cast (0), bind (myfunc, _1))" serait également acceptable et ne changerait pas l'interface fournie. – janm

5

Vous devez renvoyer un pointeur à partir de startJob() - un pointeur brut ou un pointeur intelligent approprié. Par exemple:

Token* startJob(int jobId) 
{ 
    return (jobId>0) ? new LockToken() : new Token(); 
} 

{ 
    std::auto_ptr<Token> t = startJob(jobId); 
    // ... (tasks) 
    // t falls out of scope, destructors are called 
} 

Lorsque auto_ptr est hors de portée, il appelle delete sur le pointeur enveloppé.

La solution ci-dessus est une réécriture directe de la vôtre. Comme mentionné dans une autre réponse à cette question que vous n'avez pas besoin de la classe Token mannequin du tout - il vous suffit de retourner un pointeur NULL:

LockToken* startJob(int jobId) 
{ 
    return (jobId>0) ? new LockToken() : 0; 
} 

{ 
    std::auto_ptr<LockToken> t = startJob(jobId); 
    // ... (tasks) 
    // t falls out of scope, destructors are called 
} 

auto_ptr peut être en toute sécurité attribué un pointeur NULL - il est destructor va gérer cela.

+2

+1, ou d'autres pointeurs intelligents: 'std :: tr1 :: scoped_ptr',' std :: tr1: : shared_ptr' (ou leurs alternatives de boost si votre compilateur ne supporte pas tr1) –

+0

auto_ptr est en effet une très bonne idée. Une meilleure idée aurait été que start_job retourne effectivement un tel type au lieu d'un pointeur régulier ... vous savez, juste pour être clair sur l'interface. –

2

Une approche assez typique serait de déclarer vos objets Token/LockToken sur le tas à l'aide de pointeurs.

Token* startJob(int jobID) 
{ 
    Token* t; 
    if (jobID >0) 
    { 
     t = new LockToken(); 
    } 
    else 
    { 
     t = new Token(); 
    } 

    return t; 
} 

Bien sûr, vous devez être responsable de la suppression de la valeur retournée lorsque vous avez terminé avec elle. Vous pouvez également utiliser des pointeurs intelligents, qui gèrent leur propre destruction.

1

En C++, pour obtenir un comportement polymophique, vous devez utiliser des pointeurs ou des références. Dans votre cas, comme la durée de vie du Token doit s'étendre au-delà de la fonction startJob, vous ne pouvez pas renvoyer de référence dans un objet alloué de pile interne, car à l'emplacement d'utilisation (appelant de startJob) il s'agirait d'une référence pendante.

Il ne vous reste donc plus que de la mémoire allouée dynamiquement, et à ce stade, vous pouvez choisir la manière dont vous voulez gérer la durée de vie de l'objet affecté par le tas. Je déconseille d'utiliser des pointeurs bruts car ils sont intrinsèquement dangereux, il existe déjà différentes réponses fines utilisant des pointeurs bruts comme valeur de retour et gérant le pointeur dans un pointeur intelligent, ou renvoyant déjà un pointeur intelligent. L'inconvénient de renvoyer un pointeur brut et de le gérer de manière externe dans un pointeur intelligent est qu'il est un peu plus fragile pour le code utilisateur. L'appelant peut utiliser des pointeurs intelligents, ou utiliser des pointeurs bruts (ou ignorer l'objet renvoyé) et il perdra de la mémoire. L'utilisation d'un shared_ptr dans votre interface utilisateur impose l'utilisation de ce pointeur intelligent dans le code de l'appelant (l'utilisateur ne peut pas décider de passer à un autre type de pointeur intelligent).

L'utilisation d'un bon vieux std::auto_ptr comme type de retour semble être l'approche la plus souple à ce point:

std::auto_ptr<Token> startJob(int jobId); 

void user_code() 
{ 
    std::auto_ptr<Token> job1 = startJob(1); 
    boost::shared_ptr<Token> job2(startJob(2)); // shared_ptr has a constructor taking auto_ptr 
    startJob(3); // fine: the temporary auto_ptr dies and releases the memory 
    boost::scoped_ptr<Token> job4(startJob(4).release()); // cumbersome, but feasible 
} 

(scoped_ptr référence)

Si le type retourné est un autre type de pointeur intelligent comme le type de retour, alors vous ne seriez pas en mesure de le faire céder la ressource à utiliser dans un autre type de pointeur intelligent. Si vous avez renvoyé un jeton de travail brut 3, le jeton ne sera pas libéré. Je n'ai pas considéré unique_ptr pour la discussion. Cela semble être une bonne alternative à auto_ptr, mais je ne l'ai jamais utilisé, donc je ne peux pas le dire par expérience.

+0

"En C++, pour obtenir un comportement polymorphe, vous devez utiliser des pointeurs ou des références" et "Il vous reste donc de la mémoire allouée dynamiquement". Je ne suis pas d'accord avec l'une ou l'autre de ces choses, du moins en ce qui concerne l'interface. En supposant une fonction qui fait quelque chose basé sur un job_id, vous pouvez utiliser quelque chose comme "shared_ptr (reinterpret_cast (jobId), bind (release_func, _1))".Pas de pointeurs, pas de références, pas de mémoire explicitement allouée dynamiquement (en ignorant les détails d'implémentation shared_ptr <>). – janm

+0

Dans ce cas précis, renvoyer une classe par une valeur contenant un contexte, un pointeur de fonction et un destructeur pourrait également fournir la même fonctionnalité, mais pourquoi s'inquiéter quand shared_ptr <> vous offre beaucoup plus de flexibilité? – janm

+0

@janm: 'comportement polymorphique' ne peut pas être atteint sans un pointeur ou une référence à une classe de base. Vous pouvez stocker un pointeur vide, mais vous ne pourrez pas appeler de méthode dessus. Je n'appellerais pas dispatching en termes de gestionnaire (jobId) polymorphe non plus. –

0

Merci pour toutes les réponses! J'ai choisi d'aller avec la solution de Janm à l'exception que je vais utiliser un std::auto_ptr au lieu d'un boost::shared_ptr. Notez que cela rend la solution similaire à la réponse de sharptooth (avec le commentaire de Matthieu M). Je l'ai prototypé et je l'ai intégré au reste de l'application et je suis heureux d'annoncer que cela fonctionne bien.