【问题标题】:Array of non-contiguous objects非连续对象数组
【发布时间】:2017-02-09 01:10:18
【问题描述】:
#include <iostream> 
#include <cstring>
// This struct is not guaranteed to occupy contiguous storage
// in the sense of the C++ Object model (§1.8.5):
struct separated { 
  int i; 
  separated(int a, int b){i=a; i2=b;} 
  ~separated(){i=i2=-1;} // nontrivial destructor --> not trivially   copyable
  private: int i2;       // different access control --> not standard layout
};
int main() {
  static_assert(not std::is_standard_layout<separated>::value,"sl");
  static_assert(not std::is_trivial<separated>::value,"tr");
  separated a[2]={{1,2},{3,4}};
  std::memset(&a[0],0,sizeof(a[0]));
  std::cout<<a[1].i;    
  // No guarantee that the previous line outputs 3.
}
// compiled with Debian clang version 3.5.0-10, C++14-standard 
// (outputs 3) 
  1. 削弱标准保证以致该程序可能表现出未定义行为的原因是什么?

  2. 标准说: “数组类型的对象包含一个连续分配的非空集合,由 N 个类型为 T 的子对象组成。” [dcl.array] §8.3.4。 如果类型 T 的对象不占用连续的存储空间,那么这些对象的数组怎么办?

编辑:删除可能分散注意力的说明文字

【问题讨论】:

  • 什么意思是对象不占用连续存储?您是在谈论可能在成员变量之间的填充吗?
  • 对于您的第一个问题:因为没有人愿意围绕 memset 这样的 C 东西设计 C++。 C 结构体需要与 memset 兼容以实现兼容性,其余的并不重要。
  • 这是哪里来的?你有没有运行它并没有得到3?有一条评论说“不保证......”但我不知道是谁在断言。
  • @JoachimPileborg 该标准允许实现对象所需的部分存储在完全独立的内存区域(例如 vtables)中
  • 除了对象不连续之外,还有很多很好的理由为什么memsetting 一个“复杂”对象应该是 UB。

标签: c++ arrays c++14 language-lawyer memory-layout


【解决方案1】:

1。 这是实际编写编译器的龙所采用的奥卡姆剃刀的一个实例:不要提供超过解决问题所需的保证,否则您的工作量将加倍而没有补偿。适应花哨硬件或历史硬件的复杂类是问题的一部分。 (BaummitAugen 和 M.M 暗示)

2。 (连续=共享一个共同的边界,下一个或按顺序一起)

首先,类型 T 的对象并不是总是或从不占用连续的存储空间。在单个二进制文件中,同一类型可能有不同的内存布局。

[class.derived] §10 (8): 基类子对象的布局可能不同于 ...

这足以让我们后退并确信我们计算机上发生的事情与标准不冲突。但是让我们修改一下这个问题。一个更好的问题是:

标准是否允许不单独占用连续存储空间的对象数组,同时每两个连续的子对象共享一个公共边界?

如果是这样,这将严重影响 char* 算术与 T* 算术的关系。

根据您是否理解 OP 标准引用的意思,即只有子对象共享一个公共边界,或者在每个子对象内,字节共享一个公共边界,您可能会得出不同的结论。

假设第一个,你会发现 “连续分配”或“连续存储”可能仅表示 &a[n]==&a[0] + n(第 23.3.2.1 节),这是关于子对象地址的声明,并不意味着数组驻留在单个序列中的连续字节。

如果你假设更强的版本,你可能会得出T* versus char* pointer arithmetic中提出的'element offset==sizeof(T)'结论 这也意味着人们可以通过将它们声明为 T t[1] 来强制其他可能不连续的对象进入连续布局;而不是 T t;

现在如何解决这个烂摊子?标准中对 sizeof() 运算符的定义基本上是模棱两可的,这似乎是那个时代的遗留物,至少在每个体系结构中,类型大致等于布局,现在情况不再如此。 (How does placement new know which layout to create?)

当应用于一个类时,[sizeof()] 的结果是该类对象中的字节数,包括将该类型对象放入数组中所需的任何填充。 [expr.sizeof] §5.3.3 (2)

但是等等,所需的填充量取决于布局,一个类型可能有多个布局。所以我们一定要加一粒盐,并在所有可能的布局中取最小,或者做一些同样任意的事情。

最后,如果这是预期的含义,数组定义将受益于 char* 算术方面的消歧。否则,问题 1 的答案相应适用。


关于现已删除的答案和 cmets 的一些评论: 正如Can technically objects occupy non-contiguous bytes of storage? 中所讨论的,实际上存在非连续对象。此外,天真地 memset 子对象可能会使包含对象的不相关子对象无效,即使对于完全连续的、可简单复制的对象也是如此:

#include <iostream>
#include <cstring>
struct A {
  private: int a;
  public: short i;
};
struct B :  A {
  short i;
};
int main()
{
   static_assert(std::is_trivial<A>::value , "A not trivial.");
   static_assert(not std::is_standard_layout<A>::value , "sl.");
   static_assert(std::is_trivial<B>::value , "B not trivial.");
   B object;
   object.i=1;
   std::cout<< object.B::i;
   std::memset((void*)&(A&)object ,0,sizeof(A));
   std::cout<<object.B::i;
}
// outputs 10 with g++/clang++, c++11, Debian 8, amd64     

因此,可以想象问题帖中的 memset 可能会将 a[1].i 归零,这样程序就会输出 0 而不是 3。

很少有人会在 C++ 对象中使用类似 memset 的函数。 (通常,如果您这样做,子对象的析构函数会明显失败。)但有时人们希望在其析构函数中清除“几乎是 POD”类的内容,这可能是个例外。

【讨论】:

  • 由于可以将对象放置在适当大小的适当对齐的字符数组中,似乎是的,至少“可以强制其他可能不连续的对象进入连续布局”,无论一个人对指针算术的解释。
  • 此外,由于可以手动调用析构函数,然后将新对象强制放置在现在为空的存储位置,因此似乎一个实现别无选择,只能对所有大多数情况使用相同的连续布局-相同类型的派生对象。
  • @n.m.我猜你的意思是placement-new,但是“非连续”布局仍然存在,可能有部分对象没有放在缓冲区中。 vtable 就是一个常见的例子。
  • 一种类型如何有多种布局(在一个编译器+OS+架构上)?
  • @M.M 在任何想象中,vtable 都不是对象的一部分。它通常存在于对象创建之前和销毁之后,并且在许多相同类型的对象之间共享。如果您将 vtable 称为“对象的一部分”,则将函数称为“函数指针的一部分”。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-01-15
  • 1970-01-01
  • 1970-01-01
  • 2017-09-14
  • 2016-01-31
  • 2020-03-16
  • 2013-03-03
相关资源
最近更新 更多