2009-07-13 5 views
2

Avant cela est marqué comme doublon, je suis au courant de this question, mais dans mon cas, nous parlons de conteneurs const.Pourquoi est-il impossible de transmettre un jeu de const <Derived*> comme const set <Base*> à une fonction?

J'ai 2 classes:

class Base { }; 
class Derived : public Base { }; 

Et une fonction:

void register_objects(const std::set<Base*> &objects) {} 

Je voudrais invoquer cette fonction:

std::set<Derived*> objs; 
register_objects(objs); 

Le compilateur n'accepte pas. Pourquoi pas? L'ensemble n'est pas modifiable, il n'y a donc aucun risque que des objets non dérivés y soient insérés. Comment puis-je le faire de la meilleure façon?

Edit:
Je comprends que maintenant le compilateur fonctionne d'une manière qui set<Base*> et set<Derived*> sont totalement indépendants et à cet effet la signature de la fonction ne se trouve pas. Ma question maintenant est cependant: pourquoi le compilateur fonctionne-t-il comme ceci? Y aurait-il des objections pour ne pas voir const set<Derived*> comme dérivé de const set<Base*>

Répondre

13

La raison pour laquelle le compilateur ne l'accepte pas est que la norme lui interdit de le faire.

La raison pour laquelle la norme ne le dit pas, c'est que le comité n'a pas introduit une règle selon laquelle const MyTemplate<Derived*> est un type apparenté à const MyTemplate<Base*> même si les types non-const ne sont pas liés. Et ils ne voulaient certainement pas de règle spéciale pour std :: set, car en général le langage ne fait pas de cas particuliers pour les classes de bibliothèques.

La raison pour laquelle le comité de normalisation n'a pas voulu associer ces types, c'est que MyTemplate pourrait ne pas avoir la sémantique d'un conteneur. Considérez:

template <typename T> 
struct MyTemplate { 
    T *ptr; 
}; 

template<> 
struct MyTemplate<Derived*> { 
    int a; 
    void foo(); 
}; 

template<> 
struct MyTemplate<Base*> { 
    std::set<double> b; 
    void bar(); 
}; 

Alors qu'est-ce qu'il ne signifie même passer un const MyTemplate<Derived*> comme const MyTemplate<Base*>? Les deux classes n'ont pas de fonctions membres communes et ne sont pas compatibles avec la mise en page. Vous auriez besoin d'un opérateur de conversion entre les deux, ou le compilateur n'aurait aucune idée de ce qu'il faut faire, qu'il soit const ou non. Mais la façon dont les modèles sont définis dans la norme, le compilateur n'a aucune idée de ce qu'il faut faire même sans les spécialisations de modèle.

std::set lui-même pourrait fournir un opérateur de conversion, mais cela devrait juste faire une copie (*), que vous pouvez faire vous-même assez facilement. S'il y avait une telle chose comme std::immutable_set, alors je pense qu'il serait possible d'implémenter tel qu'un std::immutable_set<Base*> pourrait être construit à partir d'un std::immutable_set<Derived*> juste en pointant vers le même pImpl. Même ainsi, des choses étranges se produiraient si des opérateurs non-virtuels étaient surchargés dans la classe dérivée - le conteneur de base appelait la version de base, donc la conversion pourrait désordonner l'ensemble s'il avait un comparateur non-par défaut les objets eux-mêmes au lieu de leurs adresses. Donc, la conversion viendrait avec de lourdes réserves. Mais de toute façon, il n'y a pas de immutable_set, et const n'est pas la même chose que immuable.

En outre, supposons que Derived soit associé à Base par héritage virtuel ou multiple. Ensuite, vous ne pouvez pas simplement réinterpréter l'adresse d'un Derived comme l'adresse d'un Base: dans la plupart des implémentations, la conversion implicite change l'adresse. Il s'ensuit que vous ne pouvez pas convertir par lots une structure contenant Derived* en tant que structure contenant Base* sans copier la structure. Mais le standard C++ permet en fait cela pour n'importe quelle classe non-POD, pas seulement avec l'héritage multiple. Et Derived est non-POD, car il a une classe de base. Donc, afin de soutenir ce changement à std::set, les principes de base de l'héritage et de la structure devraient être modifiés. C'est une limitation de base du langage C++ que les conteneurs standards ne puissent pas être réinterprétés comme vous le voulez, et je ne connais pas de trucs qui pourraient les rendre sans réduire l'efficacité ou la portabilité ou les deux. C'est frustrant, mais ce genre de choses est difficile.

Depuis votre code passe un ensemble en valeur de toute façon, vous pouvez simplement faire cette copie:

std::set<Derived*> objs; 
register_objects(std::set<Base*>(objs.begin(), objs.end()); 

[Edit: vous avez changé votre exemple de code ne pas passer par valeur. Mon code fonctionne toujours, et autant que je sache mieux que vous pouvez faire autre que de réarranger le code appelant à utiliser un std::set<Base*> en premier lieu.]

Rédaction d'une enveloppe pour std::set<Base*> qui assure tous les éléments sont Derived*, la façon dont Java génériques travail , est plus facile que d'organiser la conversion que vous voulez être efficace. Ainsi, vous pouvez faire quelque chose comme:

template<typename T, typename U> 
struct MySetWrapper { 
    // Requirement: std::less is consistent. The default probably is, 
    // but for all we know there are specializations which aren't. 
    // User beware. 
    std::set<T> content; 
    void insert(U value) { content.insert(value); } 
    // might need a lot more methods, and for the above to return the right 
    // type, depending how else objs is used. 
}; 

MySetWrapper<Base*,Derived*> objs; 
// insert lots of values 
register_objects(objs.content); 

(*) En fait, je pense que cela pourrait copier-on-write, qui dans le cas d'un paramètre const utilisé de la manière typique signifierait qu'il ne doit jamais faire la copie. Mais la copie sur écriture est un peu discréditée dans les implémentations STL, et même si ce n'était pas le cas, je doute que le comité veuille imposer un tel détail de mise en œuvre.

0

Eh bien, comme indiqué dans la question que vous mentionnez, set<Base*> et set<Derived*> sont différents objets. Votre fonction register_objects() prend un objet set<Base*>. Donc, le compilateur ne connaît pas de register_objects() qui prend set<Derived*>. La constance du paramètre ne change rien. Les solutions énoncées dans la question citée semblent les meilleures choses que vous pouvez faire. Dépend de ce que vous devez faire ...

2

std::set<Base*> et std::set<Derived*> sont fondamentalement deux objets différents. Bien que les classes Base et Derived soient liées via l'héritage, au niveau de l'instanciation du template du compilateur, elles sont deux instanciations différentes (of set).

1

Tout d'abord, il semble un peu bizarre que vous n'êtes pas de passage par référence ...

En second lieu, comme mentionné dans l'autre poste, vous seriez mieux créer le passé en jeu comme std: : définissez < Base *> puis en ajoutant une classe dérivée pour chaque membre de l'ensemble.

Votre problème vient sûrement du fait que les 2 types sont complètement différents. std :: set < Dérivé *> n'est en aucun cas hérité de std :: set < Base *> en ce qui concerne le compilateur. Ils sont tout simplement 2 différents types de jeu ...

+0

J'ai corrigé le passage de référence, c'était une faute de frappe dans ma question, dans mon code je l'ai fait correctement. –

+0

Ils ne sont pas simplement «simplement différents»: il y a sûrement une relation entre les types, seulement ce n'est pas un simple héritage. – xtofl

+0

mais la substitution de modèle fonctionne en substituant le paramètre de modèle dans la classe n'est-ce pas? Les 2 définitions peuvent être considérées comme std :: set_base_ptr et std :: set_derived_ptr, c'est-à-dire fondamentalement différentes. Si cela peut être fait, je serais très surpris, mais je serais heureux d'admettre que j'ai mal compris comment fonctionne la substitution de modèles (ce qui, sans doute, aiderait à étendre mes connaissances de modèle). – Goz

2

Si votre fonction register_objects reçoit un argument, il peut mettre/attendre une sous-classeBase là-dedans. C'est ce que la signature signifie.

C'est une violation du principe de substitution Liskov. Ce problème particulier est également désigné par Covariance. Dans ce cas, lorsque votre argument de fonction est un conteneur constant, il est possible que soit mis à l'état. Dans le cas où le conteneur d'arguments est mutable, il ne peut pas fonctionner.

2

Regardez d'abord ici: Is array of derived same as array of base.Dans votre cas, set of derived est un conteneur totalement différent de l'ensemble de base et puisqu'il n'y a pas d'opérateur de conversion implicite disponible pour convertir entre eux, le compilateur donne une erreur.

0

Comme vous le savez, les deux classes sont assez similaires une fois que vous avez supprimé les opérations non const. Cependant, en C++, l'héritage est une propriété des types, alors que const est un simple qualificateur en plus des types. Cela signifie que vous ne pouvez pas dire à juste titre que const X dérive de const Y, même quand X dérive de Y.

De plus, si X ne hérite pas de Y, qui s'applique à toutes les variantes de cv qualifié de X et Y, ainsi . Cela s'étend à std::set instanciations. Puisque std::set<Foo> n'hérite pas de std::set<bar>, std::set<Foo> const n'hérite pas non plus de std::set<bar> const.

Questions connexes