非连续对象的数组

时间:2016-09-30 12:42:46

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

#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类型的对象不占用连续存储,那么这些对象的数组如何呢?

  3. 编辑:删除可能令人分心的解释性文字

1 个答案:

答案 0 :(得分:2)

1。     这是实际编写编译器的龙所采用的奥卡姆剃刀的一个实例:不要提供比解决问题所需的更多保证,因为否则你的工作量会加倍而无需补偿。适合于硬件或历史硬件的复杂类是问题的一部分。 (由BaummitAugen和M.M暗示)

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

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

  

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

这足以让我们退缩并对我们计算机上发生的事情与标准不矛盾感到满意。但是让我们修改这个问题。一个更好的问题是:

  

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

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

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

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

如果您假设版本较强,则可以得出T* versus char* pointer arithmetic中提出的'元素偏移== 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的答案也相应适用。

与现在删除的答案和评论有关的一些评论: 正如Can technically objects occupy non-contiguous bytes of storage?中所讨论的,实际存​​在非连续对象。此外,天真地记忆子对象可能会使包含对象的不相关子对象无效,即使对于完美连续的,平凡的可复制对象也是如此:

#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可能为零[1] .i,这样程序将输出0而不是3。

很少有人会使用类似memset的函数和C ++ - 对象。 (通常情况下,如果你这样做,子对象的析构函数将会公然失败。)但是有时人们希望在析构函数中擦除“几乎POD”类的内容,这可能是例外。