为啥不能将 const set<Derived*> 作为 const set<Base*> 传递给函数?

Posted

技术标签:

【中文标题】为啥不能将 const set<Derived*> 作为 const set<Base*> 传递给函数?【英文标题】:Why is is it not possible to pass a const set<Derived*> as const set<Base*> to a function?为什么不能将 const set<Derived*> 作为 const set<Base*> 传递给函数? 【发布时间】:2009-07-13 10:52:29 【问题描述】:

在这被标记为重复之前,我知道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&lt;Base*&gt;set&lt;Derived*&gt; 完全不相关的方式工作,因此找不到函数签名。然而,我现在的问题是:为什么编译器会这样工作?不将const set&lt;Derived*&gt; 视为const set&lt;Base*&gt; 的衍生物会有任何反对意见

【问题讨论】:

【参考方案1】:

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

标准告诉它不要这样做的原因是,即使非 const 类型不相关,委员会也没有引入 const MyTemplate&lt;Derived*&gt; 是与 const MyTemplate&lt;Base*&gt; 相关类型的规则。他们当然不想要 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&lt;Derived*&gt; 作为const MyTemplate&lt;Base*&gt; 传递是什么意思?这两个类没有共同的成员函数,也不兼容布局。您需要两者之间的转换运算符,否则编译器将不知道无论它们是否为 const 该怎么做。但是标准中定义模板的方式,即使没有模板特化,编译器也不知道该怎么做。

std::set 本身可以提供一个转换运算符,但这只需要制作一个副本(*),您可以自己轻松完成。如果有 std::immutable_set 这样的东西,那么我认为有可能实现这样一个 std::immutable_set&lt;Base*&gt; 可以通过指向相同的 pImpl 从 std::immutable_set&lt;Derived*&gt; 构造。即便如此,如果您在派生类中重载了非虚拟运算符,则会发生奇怪的事情 - 基础容器将调用基础版本,因此如果它有一个非默认比较器可以执行任何操作,则转换可能会取消对集合的排序对象本身而不是它们的地址。因此,转换将伴随着严重的警告。但不管怎样,没有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());

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

std::set&lt;Base*&gt; 编写一个包装器以确保所有元素都是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 实现中有点名誉扫地,即使我不怀疑委员会会想要强制执行如此重量级的实现细节。

【讨论】:

【参考方案2】:

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

这违反了 Liskov 替换原则。

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

【讨论】:

【参考方案3】:

先看这里:Is array of derived same as array of base。在您的情况下,派生集是与基集完全不同的容器,并且由于没有隐式转换运算符可用于在它们之间进行转换,因此编译器会出错。

【讨论】:

【参考方案4】:

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

【讨论】:

【参考方案5】:

首先,您没有通过引用传递似乎有点奇怪......

其次,如另一篇文章中所述,您最好将传入的集合创建为 std::set,然后为每个集合成员新建一个 Derived 类。

您的问题肯定源于这两种类型完全不同的事实。就编译器而言,std::set 绝不是从 std::set 继承的。它们只是两种不同类型的集合...

【讨论】:

我更正了引用传递,这是我的问题中的一个错字,在我的代码中我做到了这一点。 它们不仅仅是“完全不同”:类型之间肯定存在关系,只是它不仅仅是继承。 但是模板替换通过将模板参数替换到类中来工作,不是吗?这两个定义可以认为是std::set_base_ptr 和std::set_derived_ptr,即根本不同。如果可以做到,我会感到非常惊讶,但我很乐意承认我误解了模板替换的工作原理(毫无疑问,这将有助于扩展我的模板知识)。【参考方案6】:

好吧,正如您提到的问题中所述,set&lt;Base*&gt;set&lt;Derived*&gt; 是不同的对象。您的 register_objects() 函数采用 set&lt;Base*&gt; 对象。因此编译器不知道任何采用set&lt;Derived*&gt; 的 register_objects()。参数的常量不会改变任何东西。引用问题中所述的解决方案似乎是您能做的最好的事情。取决于你需要做什么......

【讨论】:

【参考方案7】:

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

此外,如果 X 不从 Y 继承,这也适用于 X 和 Y 的所有 cv 限定变体。这扩展到std::set 实例化。因为std::set&lt;Foo&gt; 不继承自std::set&lt;bar&gt;,所以std::set&lt;Foo&gt; const 也不继承自std::set&lt;bar&gt; const

【讨论】:

【参考方案8】:

你说得对,这在逻辑上是允许的,但它需要更多的语言特性。如果您有兴趣了解另一种语言的实现方式,它们在 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

【讨论】:

【参考方案9】:

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

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

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

【讨论】:

以上是关于为啥不能将 const set<Derived*> 作为 const set<Base*> 传递给函数?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我不能分配 const 但我可以控制台记录它?

为啥我不能将 const 左值引用绑定到返回 T&& 的函数?

为啥指向函数的“const”指针不能在常量表达式中使用?

为啥不能在用 let 和 const 声明变量之前进行赋值? [复制]

对于set iterator和const_iterator的输入,不能重载成员函数(但可以用于其他stl迭代器)

const常量不能被修改,为啥编译还能通过?