非连续对象数组

Array of non-contiguous objects

#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. 标准说: "An object of array type contains a contiguously allocated non-empty set of N subobjects of type T." [dcl.array] §8.3.4。 如果类型 T 的对象不占用连续的存储空间,那么这样的对象的数组怎么办?

编辑:删除了可能分散注意力的解释性文字

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

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

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

[class.derived] §10 (8): A base class subobject might have a layout different from ...

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

Does the standard permit arrays of objects that do not occupy contiguous storage individually, while at the same time every two successive subobjects share a common border?

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

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

假设第一个,你会发现 'contiguously allocated' 或 'stored contiguously' 可能只是表示 &a[n]==&a[0] + n (§23.3.2.1),这是关于子对象地址的声明,并不意味着数组位于单个连续字节序列。

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

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

When applied to a class, the result [of sizeof()] is the number of bytes in an object of that class including any padding required for placing objects of that type in an array. [expr.sizeof] §5.3.3 (2)

等等,所需的填充量取决于布局,并且一种类型可能有多个布局。所以我们一定会有所保留并在所有可能的布局中采用最小值,或者做一些同样随意的事情。

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


关于现已删除的答案和评论的一些评论: 正如 中所讨论的,实际上存在不连续的对象。此外,天真地对子对象进行内存设置可能会使包含对象的不相关子对象无效,即使对于完全连续的、可简单复制的对象也是如此:

#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     

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

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