为什么不能将const set <derived *>作为const set <base *>传递给函数?</base *> </derived *>

时间:2009-07-13 10:52:30

标签: c++ inheritance stl

在此之前被标记为重复,我知道this问题,但在我的情况下,我们讨论的是const容器。

我有2个班级:

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

一个功能:

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

我想将此函数调用为:

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

编译器不接受此操作。为什么不?该集合不可修改,因此不存在将非派生对象插入其中的风险。我怎样才能以最好的方式做到这一点?

修改
我知道现在编译器以set<Base*>set<Derived*>完全不相关的方式工作,因此找不到函数签名。我现在的问题是:编译器为什么这样工作?如果不将const set<Derived*>视为const set<Base*>

的衍生产品,会有任何异议吗?

9 个答案:

答案 0 :(得分:13)

编译器不接受这个的原因是标准告诉它不要。

标准告诉它的原因是委员会没有引入const MyTemplate<Derived*>const MyTemplate<Base*>是相关类型的规则,即使非const类型不相关。他们当然不想要std :: set的特殊规则,因为一般来说语言不会为库类做出特殊情况。

标准委员会不希望使这些类型相关的原因是MyTemplate可能没有容器的语义。考虑:

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();
};

那么将const MyTemplate<Derived*>作为const MyTemplate<Base*>传递是什么意思呢?这两个类没有共同的成员函数,并且不是布局兼容的。你需要在两者之间使用转换运算符,否则编译器不知道该怎么做它们是不是const。但是标准中定义了模板的方式,即使没有模板特化,编译器也不知道该做什么。

std::set本身可以提供转换运算符,但这只需要制作副本(*),您可以轻松完成。如果存在std::immutable_set这样的事情,那么我认为可以实现这样的事情,即std::immutable_set<Base*>只能通过指向同一个pImpl从std::immutable_set<Derived*>构造。即便如此,如果您在派生类中重载了非虚拟运算符,也会发生奇怪的事情 - 基本容器会调用基本版本,因此如果转换有非默认比较器,那么转换可能会对集合进行降序。对象本身而不是他们的地址。所以转换会带来沉重的警告。但无论如何,没有immutable_set,而const与immutable不同。

此外,假设Derived通过虚拟或多重继承与Base相关。然后,您不能只将Derived的地址重新解释为Base的地址:在大多数实现中,隐式转换会更改地址。因此,您不能仅将包含Derived*的结构批量转换为包含Base*的结构,而无需复制结构。但是C ++标准实际上允许任何非POD类发生这种情况,而不仅仅是多重继承。 Derived是非POD,因为它有一个基类。因此,为了支持对std::set的这种更改,必须改变继承和结构布局的基础。这是C ++语言的一个基本限制,标准容器无法以您想要的方式重新解释,而且我不知道任何可以在不降低效率或可移植性的情况下使其成为可能的技巧。这令人沮丧,但这件事很难。

由于您的代码无论如何都是按值传递,您只需制作该副本:

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

[编辑:您已更改代码示例,而不是按值传递。我的代码仍然有效,除了重构调用代码以便首先使用std::set<Base*>之外,afaik是你能做的最好的事情。]

std::set<Base*>编写一个包装器,确保所有元素都是Derived*,这与Java泛型的工作方式相比,比安排想要高效的转换更容易。所以你可以这样做:

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);

(*)实际上,我猜它可以写入时复制,在典型方式中使用const参数的情况下,它意味着它永远不需要复制。但是在STL实现中,copy-on-write有点不可信,即使不是我怀疑委员会是否会要求强制执行这样一个重量级的实现细节。

答案 1 :(得分:2)

std::set<Base*>std::set<Derived*>基本上是两个不同的对象。虽然Base和Derived类是通过继承链接的,但在编译器模板实例化级别它们是两个不同的实例化(set)。

答案 2 :(得分:2)

如果您的register_objects函数收到一个参数,它可以在其中放置/期望任何 Base子类。这就是它的签名sais。

这违反了Liskov替代原则。

这个特殊问题也称为Covariance。在这种情况下,函数参数是一个常量容器,可以使用。如果参数容器是可变的,它就无法工作。

答案 3 :(得分:2)

首先来看看Is array of derived same as array of base。在你的情况下,派生集是一个完全不同的容器集合,因为没有隐式转换运算符可用于它们之间的转换,编译器给出了错误。

答案 4 :(得分:1)

首先,你没有通过参考传递......似乎有点奇怪。

其次,正如另一篇文章中所提到的,最好将传入的集创建为std :: set&lt;基数*&gt;然后为每个集成员新建一个Derived类。

你的问题肯定是因为这两种类型完全不同。的std ::设置&LT;派生*&gt;绝不继承自std :: set&lt;基数*&gt;就编译器而言。它们只是两种不同类型的套装......

答案 5 :(得分:0)

嗯,正如您提到的问题所述,set<Base*>set<Derived*>是不同的对象。 register_objects()函数接受set<Base*>个对象。所以编译器不知道任何带set<Derived*>的register_objects()。参数的常量不会改变任何东西。引用问题中陈述的解决方案似乎是您可以做的最好的事情。取决于你需要做什么...

答案 6 :(得分:0)

如您所知,一旦删除非const操作,这两个类就非常相似。但是,在C ++中,继承是类型的属性,而const仅仅是类型之上的限定符。这意味着,即使X派生自Y,您也无法正确说明const X派生自const Y

此外,如果X不从Y继承,那么它也适用于X和Y的所有cv限定变体。这扩展到std::set实例化。由于std::set<Foo>不会从std::set<bar>继承,std::set<Foo> const也不会从std::set<bar> const继承。

答案 7 :(得分:0)

你是对的,这在逻辑上是允许的,但它需要更多的语言功能。它们在C#4.0中可用,如果您有兴趣看到另一种语言的方式。见这里:http://community.bartdesmet.net/blogs/bart/archive/2009/04/13/c-4-0-feature-focus-part-4-generic-co-and-contra-variance-for-delegate-and-interface-types.aspx

答案 8 :(得分:0)

还没有看到它链接,所以这里是与此相关的C ++ FAQ Lite中的一个要点:

http://www.parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.3

我认为他们的Bag-of-Apples!= Bag of of Fruit类比适合这个问题。