【问题标题】:Container covariance in C++C++ 中的容器协方差
【发布时间】:2011-01-26 17:19:22
【问题描述】:

我知道 C++ 不支持容器元素的协方差,就像在 Java 或 C# 中一样。所以下面的代码可能是未定义的行为:

#include <vector>
struct A {};
struct B : A {};
std::vector<B*> test;
std::vector<A*>* foo = reinterpret_cast<std::vector<A*>*>(&test);

毫不奇怪,当我建议这是another question 的解决方案时,我收到了反对票。

但是 C++ 标准的哪一部分确切地告诉我这将导致未定义的行为?保证std::vector&lt;A*&gt;std::vector&lt;B*&gt; 都将它们的指针存储在一个连续的内存块中。还保证sizeof(A*) == sizeof(B*)。最后,A* a = new B 是完全合法的。

那么,我想到了标准中的哪些不良情绪(风格除外)?

【问题讨论】:

  • 在此之后使用 reinterpret_cast() 没有定义任何内容。它可能有效,但您的条件列表非常短。我会添加另外几个前提条件。大小(A)==大小(B); A 或 B 都不能包含任何类型的虚函数。 A 和 B 以及放置在数组中的任何后代都不能使用多重继承。
  • 非 C++ 特定的答案是它不是类型安全的。如果将A 添加到 foo 中,则测试处于无效状态,因为它保证所有元素都是B 类型。 C# 也不支持这个。 C# 仅支持以安全方式使用的泛型参数(仅输入或输出)并且仅在接口和委托上使用。 Java 支持它,因为它添加了运行时检查并在内部处理对象基类。
  • @CodeInChaos:是什么让你这么认为?整个想法是有缺陷的,会破坏类型系统。存在一个简单的测试here。泛型允许函数参数中的协变和逆变,这取决于您想要做什么,例如在void append( Vector&lt;? super Derived&gt; v ) { v.add( new Derived() ); }void extract( Vector&lt;? extends Base&gt; v ) { Base b = v.get(0); } 中,但它不允许转换引用。
  • 函数中允许使用的原因是泛型只是编译时类型检查,并且泛型类型从二进制文件中删除。当您在函数中使用协变/逆变参数时,编译器可以检查函数内部的操作是否不会破坏接口的要求。在调用方,它可以检查相同的要求,然后将始终作为非泛型Vector(包含Object)的引用传递。另一方面,如果允许转换,那么您将能够将Base 对象添加到Derived 的容器中。

标签: c++ covariance


【解决方案1】:

此处违反的规则记录在 C++03 3.10/15 [basic.lval] 中,它指定了非正式地称为“严格别名规则”的内容

如果程序尝试通过以下类型之一以外的左值访问对象的存储值,则行为未定义:

  • 对象的动态类型,

  • 对象的动态类型的 cv 限定版本,

  • 对象的动态类型对应的有符号或无符号类型,

  • 对应于对象动态类型的 cv 限定版本的有符号或无符号类型,

  • 在其成员中包含上述类型之一的聚合或联合类型(递归地包括子聚合或包含联合的成员),

  • 一种类型,它是对象的动态类型的(可能是 cv 限定的)基类类型,

  • char 或 unsigned char 类型。

简而言之,给定一个对象,您只能通过具有列表中一种类型的表达式访问该对象。对于没有基类的类类型对象,例如std::vector&lt;T&gt;,基本上您只能使用第一个、第二个和最后一个项目符号中命名的类型。

std::vector&lt;Base*&gt;std::vector&lt;Derived*&gt; 是完全不相关的类型,您不能像使用std::vector&lt;Derived*&gt; 一样使用std::vector&lt;Base*&gt; 类型的对象。如果您违反此规则,编译器可能会做各种事情,包括:

  • 对一个比另一个执行不同的优化,或者

  • 以不同的方式布置内部成员,或

  • 假设std::vector&lt;Base*&gt;* 永远不能与std::vector&lt;Derived*&gt;* 引用相同的对象,则执行优化

  • 使用运行时检查来确保您没有违反严格的别名规则

[它也可能不做这些事情并且它可能“工作”,但不能保证它会“工作”,如果你改变编译器或编译器版本或编译设置,它可能会停止“工作”。我在这里使用恐吓引号是有原因的。 :-)]

即使您只有一个 Base*[N],您也不能像使用 Derived*[N] 一样使用该数组(尽管在这种情况下,使用可能会更安全,其中“更安全”意味着“仍然未定义但不太可能给你惹麻烦)。

【讨论】:

  • @James:感谢您的光临 ;-) 哇,您的最后一条评论让我大吃一惊。在使用它 15 年后,我不认为自己是 C++ 新手,但我永远不会想到这(无效行为)甚至适用于数组!谢谢。
  • 如果尺寸不同,它(Base[N] vs Derived[N])肯定会给你带来麻烦,例如如果派生类有更多成员。
  • @etarion:我们在这里讨论指针:例如,f(Base a[]) { ... } 并将其(通过reinterpret_cast)传递给Derived 的数组。
  • @etarion:哎呀。我的意思是Base*[N]Derived*[N],以匹配OP 对std::vector&lt;Base*&gt;std::vector&lt;Derived*&gt; 的使用。
  • f(Base a[]) 有同样的问题。 f(Base* a[]) 是不同的情况,请参阅 James 的编辑。
【解决方案2】:

容器中协方差的一般问题如下:

假设你的演员表可以工作并且是合法的(它不是但让我们假设它适用于以下示例):

#include <vector>
struct A {};
struct B : A { public: int Method(int x, int z); };
struct C : A { public: bool Method(char y); };
std::vector<B*> test;
std::vector<A*>* foo = reinterpret_cast<std::vector<A*>*>(&test);
foo->push_back(new C);
test[0]->Method(7, 99); // What should happen here???

所以您还重新解释了将 C* 转换为 B*...

实际上我不知道 .NET 和 Java 是如何管理这个的(我认为它们在尝试插入 C 时会抛出异常)。

【讨论】:

  • 好点。虽然我知道 foo 不能被修改的事实。我应该将其声明为const
  • Java 和 C# 阻止它。这就是List&lt;? extends Base&gt; 的意义所在:当x 具有这种类型时,您不能调用x.add(someBaseObject)
【解决方案3】:

你在唤起 reinterpret_cast 的坏精神。

除非你真的知道你在做什么(我的意思是不骄傲也不迂腐)reinterpret_cast 是邪恶之门之一。

我知道的唯一安全用途是管理 C++ 和 C 函数调用之间的类和结构。 不过可能还有其他一些。

【讨论】:

  • 另一个合理的用途是利用浮点数表示的“快速数学”近似值。近似值的要求基本上消除了风险,例如fast inv sqrt (众所周知)专门在 32-bit floating-point number[s] in IEEE 754 floating-point format 上工作。 TL;DR 除非您是专业的“nasal demon”牧马人,否则请避开。
【解决方案4】:

我认为展示比讲述更容易:

struct A { int a; };

struct Stranger { int a; };

struct B: Stranger, A {};

int main(int argc, char* argv[])
{
  B someObject;
  B* b = &someObject;

  A* correct = b;
  A* incorrect = reinterpret_cast<A*>(b);

  assert(correct != incorrect); // troubling, isn't it ?

  return 0;
}

此处显示的(特定)问题是,在进行“正确”转换时,编译器会根据对象的内存布局添加一些指针调整。在reinterpret_cast 上,不执行任何调整。

我想你会明白为什么应该通常在代码中禁止使用reinterpet_cast...

【讨论】:

  • 是的,在多重继承的情况下,这增加了麻烦。不过,这对于单继承来说不是问题。
  • @Daniel:除非你使用virtual继承...除非你的基类没有虚方法而派生类有(在大多数实现上);标准不保证,因此这是一个等待中的错误。
猜你喜欢
  • 2017-08-23
  • 1970-01-01
  • 2012-08-03
  • 2013-06-18
  • 2011-03-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多