Class 布局:所有从同一个最派生的多态 class 类型创建的对象是否共享一个唯一的内存布局?

Class layout: Do all objects created from the same most derived polymorphic class type share a unique memory layout?

来自https://en.cppreference.com/w/cpp/language/data_members#Layout

Layout: When an object of some class C is created, each non-static data member of non-reference type is allocated in some part of the object representation of C.

Q1:对于(可能是虚拟的)基 class 的对象和(可能是虚拟的)基 class 的非静态数据成员也是如此吗?

Q2:如果Q1的答案为真,布局是否相同 来自 的每个对象最派生 class?例如,从 class C 创建的所有对象是否共享一个唯一的布局(例如数据成员偏移量和虚拟基的 vtable),甚至在多个编译单元之间?

Q3:如果Q2的答案为真,通过在最派生的对象地址上加上偏移量来访问数据成员安全吗class? (我想是的,因为将非函数指针转换为 char* 然后转换回来是安全的。)

关于更详细和更笼统的内容,以下代码段是否可以保证 运行 安全? (注意:由于这段代码有点长,除非你需要更详细的Qs,否则你不必阅读它。你也可以发表评论,让我编辑更好。)

谢谢。

// test_layout.cpp

#include <cassert>
#include <cstdint>
#include <utility>
#include <iostream>
#include <vector>

template<class T>
char* cast(T* p) {
  return reinterpret_cast<char*>(p);
}

void on_constructing_X(char*);

class X {
public:
  X() {
    x_ = ++counter_;
    // do init
    on_constructing_X(cast(this));
  }

  void do_something() {
    // bala bala
    std::cout << "X@" << this << " " << x_ << std::endl;
  }

private:
  int x_;
  // bala bala

  static int counter_;
};
int X::counter_ = 0;

struct Info {
  char* begin;
  char* end;
  std::vector<long>* offsets;
  bool init;
};

static std::vector<Info> constructing_wrapper;

template<typename T>
class Wrapper {
private:
  union {
    T data_;
    char dummy_;
  };
  static bool init_;
  static std::vector<long> offsets_;

private:
  template<typename...Args>
  Wrapper(Args&&...args): dummy_(0) {
    std::cout << "constructing Wrapper at " << this << std::endl;
    constructing_wrapper.push_back({cast(this), cast(this) + sizeof(*this), &offsets_, init_});
    new (&data_) T(std::forward<Args>(args)...);
    constructing_wrapper.pop_back();
    init_ = true;
  }

public:
  ~Wrapper() {
    data_.~T();
  }

  template<typename...Args>
  static Wrapper* Make(Args&&...args) {
    return new Wrapper(std::forward<Args>(*args)...);
  }

  template<typename F>
  void traversal_X(F&& f) {
    for (auto off: offsets_) {
      f(reinterpret_cast<X*>(cast(this) + off));
    }
  }

};

template<typename T>
bool Wrapper<T>::init_ = false;

template<typename T>
std::vector<long> Wrapper<T>::offsets_;

void on_constructing_X(char* x) {
  if (!constructing_wrapper.empty()) {
    auto i = constructing_wrapper.back();
    if (i.begin <= x && x + sizeof(X) <= i.end) {
      if (!i.init) {
        i.offsets->push_back(x - i.begin);
      } else {
        bool found = false;
        for (auto off: *i.offsets) {
          if (x - i.begin == off) {
            found = true;
            break;
          }
        }
        if (!found) {
          std::cout << "Error" << std::endl;
          std::abort();
        }
      }
    }
  }
}

namespace test {
  class B { X xb; };
  class D1: B { X xd1; };
  class D2: protected virtual B { X xd2; };
  class D3: protected virtual B { X xd3; };
  class DD: D1, D2, D3 { X xdd; };

  void test() {
    for (int i = 0; i < 2; ++i) {
      auto p = Wrapper<D2>::Make();
      p->traversal_X([](X* x) {x->do_something();});
      delete p;
    }
    for (int i = 0; i < 2; ++i) {
      auto p = Wrapper<DD>::Make();
      p->traversal_X([](X* x) {x->do_something();});
      delete p;
    }
  }
}

int main() {
  test::test();
  return 0;
}

根据定义,对象表示包含对象中存储的所有数据,包括所有非静态数据成员和基 classes,无论是否为虚拟。 对象表示在C++17中的定义是:

the sequence of N unsigned char objects taken up by the object of type T, where N equals sizeof(T).

显然,任何用于存储子对象的字节都是对象"taken up",因为子对象是对象的一部分。这意味着这些字节是对象表示的一部分。这应该有望回答您的问题 1。

对于Q2,我不认为标准保证相同类型的所有完整对象都具有相同的布局,除非类型是标准布局。在实践中,我认为找到一个相同类型的完整对象可能具有不同布局的实现是不寻常的。如果每个翻译单元都看到相同的 class 定义(它应该如此,否则你就违反了 ODR)那么编译器在每个翻译单元中生成相同的布局应该没有问题,这看起来就像明智的做法(否则你可能不得不生成多个虚表)。但是,如果出于某种原因实现确实想要改变布局,我认为它可以这样做,即使在单个翻译单元内也是如此。

但另外我会质疑是否允许指针运算,即使布局得到保证!如果 T 既不是标准布局也不是普通可复制类型,那么我不清楚是否允许使用 char* 在类型 T 的对象中进行指针运算] 指向 T 的字节之一。例如,考虑 offsetof 只保证支持标准布局类型,并且 memcpy 将对象转换为字节数组并返回只保证对普通可复制类型进行良好定义。假设一个类型有一个虚拟基础 class,使其既不是标准布局也不是平凡可复制的。我不确定这段代码是否定义明确:

struct B {};
struct D : virtual B {};
auto foo() {
    D d;
    B* pb = &d;
    return reinterpret_cast<char*>(pb) - reinterpret_cast<char*>(&d);
}

如果调用 foo 甚至都没有明确定义,那么它是否总是 returns 相同值的问题显然没有实际意义。也许这些类型中的偏移量是不可观察的(根据抽象机的规则)。