2011-08-28 4 views
3

Disons que j'ai une classe de base abstraite avec un virtuel pur qui retourne un objet coûteux. Comme c'est un objet cher, je devrais retourner une référence à celui-ci.C++ Covariance et références

Mais la vie n'est pas si simple, disons que j'en ai deux classes: on a la fonction appelée souvent, il est donc plus efficace de stocker une copie dans l'instance et de renvoyer une référence. L'autre est rarement appelé, il est donc préférable de créer l'objet à la demande pour sauvegarder la RAM.

Je pensais que je pouvais utiliser covariance parce que le principe de substitution Liskov serait heureux, mais bien sûr Obj n'est pas un sous-type de Obj&, donc compilent entraînent des erreurs.

class abc 
{ 
public: 
    virtual BigObj& obj() = 0; 
}; 

class derived : public abc 
{ 
public: 
    ... 
    virtual BigObj obj() { return obj_; } 

private: 
    BigObj obj_; 
}; 

Résultats dans:

conflicting return type specified for ‘virtual BigObj derived::obj()’ 

Y at-il une solution plus élégante à ce que tout simplement choisir le moins pire?

+1

La réponse simple est * vous ne voulez pas faire cela *. Vous pouvez * penser * que vous le voulez, mais vous ne voulez pas vraiment le faire. La modification de la sémantique de renvoi d'une valeur ou d'une référence est trop importante pour être * masquée * du code utilisateur. –

Répondre

4

Une solution consiste à créer une classe de pointeur intelligent pour gérer BigObj* s:

class BigObjPtr { 
public: 
    BigObjPtr(bool del, BigObj* ptr) : del(del), ptr(ptr) { } 

    BigObj* operator->() { 
     return ptr; 
    } 

    virtual ~BigObjPtr() { 
     if (del) delete ptr; 
    } 

private: 
    BigObj* ptr; 
    bool del; 
}; 

changer ensuite vos cours pour retourner un de ces derniers, et régler la del bool si vous voulez que le BigObjPtr de détruire son pointeur quand il est hors de portée:

class abc 
{ 
public: 
    virtual BigObjPtr obj() = 0; 
}; 

class derived : public abc 
{ 
public: 
    ... 
    BigObjPtr obj() { return BigObjPtr(false, &obj_); } 

private: 
    BigObj obj_; 
}; 

class otherderived : public abc 
{ 
public: 
    ... 
    BigObjPtr obj() { return BigObjPtr(true, new BigObj); } 
}; 

vous seriez bien sûr besoin de gérer la copie de BigObjPtr s, etc., mais je laisse cela à vous.

+1

Vous et Benjamin Lindley êtes sur place, bonne réponse. – cmannett85

+0

Le motif 'if (del) delete del;' marque la redondance: La valeur par défaut 'delete' est bien définie sur les pointeurs NULL. Juste 'delete del;' fonctionne. –

+0

@Konrad ce n'est pas 'si (del) delete del;' c'est 'if (del) delete ptr;' donc je ne vérifie pas si 'ptr' est' NULL'. Donc ça ne fait pas ce que tu penses :) –

2

Renvoyer un shared_ptr<BigObj> à la place. Une classe peut conserver sa propre copie, l'autre peut la créer à la demande.

1

Vous avez deux options:

  1. Comme vous l'avez dit, vous pouvez choisir le moins pire. C'est-à-dire, choisissez BigObj ou BigObj& pour les deux classes dérivées.

  2. Vous pouvez ajouter de nouvelles méthodes aux classes dérivées qui ont les types de retour appropriés. par exemple, BigObj& obj_by_ref() et BigObj obj_by_val().

La raison pour laquelle vous ne pouvez pas avoir les deux est parce que vous pourriez avoir un pointeur vers abc directement. Il spécifie BigObj&, donc peu importe quelle classe fournit l'implémentation, il vaut mieux retourner un BigObj&, parce que c'est ce que le site d'appel attend. Si une sous-classe se conduisant mal renvoyait un BigObj directement, cela causerait un chaos quand le compilateur essaierait de l'utiliser comme référence!

0

Le retour par référence est dangereux dans la plupart des cas, car il peut être difficile de suivre les problèmes de mémoire, par exemple lorsque l'objet parent est hors de portée ou supprimé. Je voudrais refaire la BigObj pour être une classe de délégué simple (ou conteneur) qui détient réellement le pointeur sur l'objet cher.

+0

Si vous ne faites pas le suivi de la propriété (shared_ptr'), un pointeur ne vous donnera pas plus de sécurité qu'une référence: lorsque l'objet parent est hors de portée, le destructeur est appelé et libère (espérons-le) les pointeurs qu'il possède. –

3

Vous devez repenser vos hypothèses, l'interface des fonctions doit être définie en termes de ce qu'est la sémantique. Donc, la question principale est quelle est la sémantique de la fonction?

Si votre fonction crée un objet, quel que soit grand ou petit, il est de copier, vous devez retourner en valeur quelle que soit la fréquence à laquelle le code est appelé. Si, d'un autre côté, vous donnez accès à un objet déjà existant, vous devez renvoyer une référence à l'objet, dans les deux cas.

Notez que:

expensive function() { 
    expensive result; 
    return result; 
} 
expensive x = function(); 

peut être optimisé par le compilateur en un seul objet expensive (il peut éviter la copie de result à l'objet retourné et elide la copie de l'objet retourné dans x). Sur le principe de substitution de Liskov, vous ne suivez pas ici, un type renvoie un objet, et l'autre renvoie une référence à un objet, qui sont des choses complètement différentes à bien des égards, même si vous le pouvez appliquer opérations similaires aux deux types retournés, le fait est qu'il existe d'autres opérations qui ne sont pas identiques et il existe différentes responsabilités qui sont transmises à l'appelant.

Par exemple, si vous modifiez l'objet retourné dans le cas de référence, vous modifiez la valeur de tous retourné des objets de la fonction à l'avenir, alors que dans le cas de la valeur de l'objet est propriété par l'appelant et peu importe ce que fait l'appelant à sa copie, l'appel suivant à la fonction retournera un objet nouvellement créé sans ces changements.

Encore une fois, pensez sur quelle est la sémantique de votre fonction et de l'utiliser pour définir ce qu'il faut retourner dans toutes les classes dérivées. Si vous n'êtes pas sûr du coût de ce code, vous pouvez revenir avec un cas d'utilisation simplifié et nous pouvons discuter de la façon d'améliorer les performances de l'application. Pour cela, vous aurez besoin d'être explicite dans ce que le code utilisateur fait avec les objets qu'il obtient, et ce qu'il attend de la fonction.

+0

Point intéressant! J'aurais dû donner à la page Wikipedia LSP plus de coup d'œil rapide ... – cmannett85