2010-11-12 9 views
3

Quel est le meilleur modèle d'utilisation de membre de données polymorphes en C++? Le plus simple que je connaisse est d'utiliser un simple pointeur.C++ Modèle de meilleure pratique d'utilisation de membre de données polymorphes

Par exemple, si le type de membre de données ne sont pas polymorphes:

class Fruit { 
    public: 
    int GetWeight() { return 1; } 
}; 

class Food { 
    public: 
    Food(Fruit f=Fruit()) : m_fruit(f) {} 
    void SetFruit(Fruit f) { m_fruit = f; } 
    Fruit GetFruit() { return m_fruit; } 
    int Test() { return m_fruit.GetWeight(); } 
    private: 
    Fruit m_fruit; 
}; 
// Use the Food and Fruit classes 
Food food1; //global scope 
void SomeFunction() { 
    Fruit f; 
    food1.SetFruit(f); 
} 
int main() { 
    Fruit fruit1, fruit2; 
    Food food2(fruit1); 
    food2.SetFruit(fruit2); 
    food2.SetFruit(Fruit()); 
    SomeFunction(); 
    food1.Test(); 
    food2.Test(); 
    return 0; 
} 

Il est facile d'utiliser les classes de plusieurs façons avec possibilité minimum d'erreurs.

Maintenant, si nous faisons Fruit classe de base (utilisez polymorphes), c'est le plus simple/meilleur que je peux penser à:

class Fruit { 
    public: 
    virtual int GetWeight() { return 0; } 
}; 

class Apple : public Fruit { 
    public: 
    virtual int GetWeight() { return 2; } 
}; 

class Banana : public Fruit { 
    public: 
    virtual int GetWeight() { return 3; } 
}; 

class Food { 
    public: 
    Food(Fruit& f=defaultFruit) : m_fruit(&f) {} 
    void SetFruit(Fruit& f) { m_fruit = &f; } 
    Fruit& GetFruit() { return *m_fruit; } 
    int Test() { return m_fruit->GetWeight(); } 
    private: 
    Fruit* m_fruit; 
    static const Fruit m_defaultFruit = Fruit(); 
}; 
// Use the Food and Fruit classes 
Food food1; //global scope 
void SomeFunction() { 
    Apple a; 
    food1.SetFruit(a); 
} 

int main() { 
    Apple a; 
    Banana b; 
    Food food2(a); 
    food2.SetFruit(b); 
    food2.SetFruit(Apple()); //Invalid ?! 
    SomeFunction(); //Weakness: the food1.m_fruit points to invalid object now. 
    food1.Test(); //Can the compiler detect this error? Or, can we do something to assert m_fruit in Food::Test()? 
    food2.Test(); 
    return 0; 
} 

Y at-il une meilleure façon de le faire? J'essaie de rester simple en n'utilisant aucune nouvelle instruction/suppression. (Note: doit être en mesure d'avoir une valeur par défaut)

MISE À JOUR: Lets Prenons un exemple plus pratique:

class Personality { 
public: 
    void Action(); 
}; 
class Extrovert : public Personality { 
    ..implementation.. 
}; 
class Introvert : public Personality { 
    ..implementation.. 
}; 

class Person { 
    ... 
    void DoSomeAction() { personality->Action(); } 
    ... 
    Personality* m_personality; 
}; 

L'objectif est d'avoir des personnes avec des personnalités différentes, et pour le rendre facile à ajouter plus de personnalité par sous-classe. Quelle est la meilleure façon de mettre en œuvre cela? Puisque, logiquement, nous n'avons besoin que d'un objet pour chaque type de Personnalité, cela n'a pas beaucoup de sens pour une Personne de copier ou d'assumer la propriété d'un objet Personnalité. Pourrait-il y avoir une différence d'approche/conception qui ne nous oblige pas à faire face à un problème de copie/propriété?

Répondre

3

Dans ce cas, il vaut mieux utiliser des constructeurs de copie et de défaut séparés. En outre, vous ne devriez pas prendre l'adresse de quelque chose passé par référence; ce pourrait être un objet sur la pile qui disparaîtra bientôt. Dans le cas de votre fonction:

void SomeFunction() { 
    Apple a; 
    food1.SetFruit(a); 
} 

a (et donc &a ainsi) devient invalide lorsque la fonction retourne, donc food1 contiendraient un pointeur non valide.

Étant donné que vous stockez un pointeur, vous devez vous attendre à ce que le Food devienne propriétaire de la pièce transmise Fruit.

class Food { 
    public: 
    Food() : m_fruit(new Fruit()) {} 
    Food(Fruit* f) : m_fruit(f) {} 
    void SetFruit(Fruit* f) { if(m_fruit != f) { delete m_fruit; m_fruit = f; } } 
    Fruit& GetFruit() { return *m_fruit; } 
    const Fruit& GetFruit() const { return *m_fruit; } 
    int Test() { return m_fruit->GetWeight(); } 
    private: 
    Fruit* m_fruit; 
}; 

L'autre option est pour Food de faire une copie du Fruit il est passé. C'est difficile. Dans l'ancien temps, nous définissons une méthode Fruit::clone() qui sous-classes emporteraient:

class Fruit { 
    public: 
    virtual int GetWeight() { return 0; } 
    virtual Fruit* clone() const { return new Fruit(*this); } 
}; 

class Apple : public Fruit { 
    public: 
    virtual int GetWeight() { return 2; } 
    virtual Fruit* clone() const { return new Apple(*this); } 
}; 

class Banana : public Fruit { 
    public: 
    virtual int GetWeight() { return 3; } 
    virtual Fruit* clone() const { return new Banana(*this); } 
}; 

Je ne pense pas que vous pouvez faire un constructeur virtuel dans la façon dont vous auriez besoin de faire ce plus joli.

Ensuite, les Food changements de classe à:

class Food { 
    public: 
    Food() : m_fruit(new Fruit()) {} 
    Food(const Fruit& f) : m_fruit(f.clone()) {} 
    void SetFruit(const Fruit& f) { delete m_fruit; m_fruit = f.clone(); } 
    Fruit& GetFruit() { return *m_fruit; } 
    const Fruit& GetFruit() const { return *m_fruit; } 
    int Test() { return m_fruit->GetWeight(); } 
    private: 
    Fruit* m_fruit; 
}; 

Avec cette configuration, votre SomeFunction travailleraient depuis food1 ne dépend plus de l'existence de a.

+0

+1 Belle description claire. Vous pouvez utiliser la clémence du type de retour co-variant pour que les pointeurs de retour de la fonction clone de l'objet dérivé reviennent aux types dérivés ... préserve juste un peu plus de connaissances du compilateur, documente mieux ce qui se passe et évite parfois la distribution. –

+0

La faiblesse de passer la propriété de la «Fruit» passée est que nous ne pouvons pas assigner le même objet plusieurs fois. Par exemple, nous avons 'Apple * a = new Apple (certains paramètres),' et nous voulons faire 'Food f1 (a);', 'Food f2 (a);', et ainsi de suite. Alors, cela signifie-t-il que la stratégie "make a copy" est meilleure? – leiiv

+0

@Tony: Pourriez-vous me donner un exemple? Peut-être posté comme une nouvelle réponse? – leiiv

1

Il n'y a pas de manière simple et propre de le faire sans opérateur nouveau, car les différents objets peuvent occuper une quantité d'espace différente en mémoire. Ainsi, les aliments ne peuvent pas contenir de fruits, mais seulement un pointeur vers Fruit, qui à son tour devrait être alloué sur le tas. Alors voici quelques alternatives.

Jetez un oeil à la bibliothèque boost::any. Il existe précisément pour résoudre votre problème. L'inconvénient est qu'il ajoute beaucoup de dépendances Boost, et même si cela peut rendre votre code simple, il serait plus difficile à comprendre pour quiconque n'est pas familier avec la bibliothèque. En dessous, bien sûr, il fait toujours de nouveau/supprimer. Pour les faire manuellement, vous pouvez faire en sorte que Food accepte un pointeur vers Fruit, et en prendre possession en appelant delete dans son destructeur ~Food() (ou en utilisant un pointeur intelligent comme shared_ptr, scoped_ptr ou auto_ptr, pour l'avoir supprimé automatiquement). Vous devez documenter clairement que la valeur transmise DOIT être allouée avec new. (La valeur par défaut présentera un désagrément spécial, vous devrez soit vérifier, soit ajouter un drapeau, ou allouer une copie de cette valeur avec operator new ainsi.)

Vous voudrez peut-être prendre n'importe quel Fruit comme argument et en faire une copie sur le tas. Cela prend plus de travail parce que vous ne savez pas, en faisant la copie, quel fruit il est réellement. Une façon courante de le résoudre consiste à ajouter une méthode virtuelle clone() au Fruit, que Fruit Fruits doit implémenter correctement.

+0

J'ai réalisé que je viens de décrire en gros les mots de la solution de Mike DeSimone (publiée en même temps). Cela montre que c'est effectivement la manière habituelle de résoudre cela. –

0

Votre SetFruit() assigne l'adresse « f à m_fruit, pas l'adresse de tout ce qui a été transmise. Depuis f est juste un argument de fonction, il existe sur la pile jusqu'à ce que SetFruit() retours. Après cela, f s'en va et son adresse n'est plus valide. Ainsi, m_fruit pointe vers quelque chose qui n'existe pas.

Ainsi, au lieu, faire quelque chose comme ceci:

class Food { 
    public: 
    Food(Fruit* f = &defaultFruit) : m_fruit(f) {} 
    void SetFruit(Fruit* f) { m_fruit = f; } 
    Fruit* GetFruit() { return m_fruit; } 
    int Test() { return m_fruit->GetWeight(); } 
    private: 
    Fruit* m_fruit; 
    static const Fruit m_defaultFruit; 
}; 

Polymorphisme exige que vous faites tout avec pointeurs (ou références que certains paramètres, mais cela ne correspond pas à votre cas).

+0

SetFruit (Fruit & f), f est juste une référence à l'objet passé, donc je pense que & f donnera l'adresse de tout objet passé. Es-tu sûr de ça? – leiiv

+0

@leiiv Sentez-vous libre de personnaliser à la fois & f et & a si vous êtes sceptique. – chrisaycock

+0

Je viens de l'essayer en utilisant un simple test, et il m'a donné la même adresse. – leiiv