非连续对象的数组

Array of non-contiguous objects

本文关键字:数组 对象 连续      更新时间:2023-10-16
#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。数组)§8.3.4。如果类型T的对象不占用连续存储,那么这种对象的数组如何占用连续存储呢?

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

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

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

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

[class.derived]§10(8):一个基类的子对象可能有一个不同于…

这足以让我们放松下来,并对计算机上发生的事情不违反标准感到满意。让我们修正一下这个问题。更好的问题应该是:

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

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

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

假设是前者,你会发现'连续分配'或'连续存储'可以简单地表示&a[n]==&a[0] + n(§23.3.2.1),这是一个关于子对象地址的语句,并不意味着数组驻留在单个连续字节序列中。

如果你采用更强的版本,你可能会得到T*与char*指针算法中提出的'element offset==sizeof(T)'结论这也意味着我们可以通过声明T[1]来强制不相邻的对象进入相邻的布局;代替T T;

现在如何解决这个混乱?在标准中,sizeof()操作符的定义从根本上是模糊的,这似乎是至少在每个体系结构中,类型大致等于布局的时代的遗留物,而现在已经不是这样了。(placement new如何知道该创建哪个布局?)

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

但是等等,所需的填充量取决于布局,单个类型可能有多个布局。所以我们必须在所有可能的布局中选择最小值,或者做一些同样武断的事情。

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


与现在删除的答案和评论有关的一些备注:正如在技术上对象可以占用非连续字节存储中所讨论的那样?,不连续对象实际上是存在的。此外,简单地对子对象进行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可能为0 a[1]。I,这样程序将输出0而不是3。

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