Макет класса: все ли объекты, созданные из одного и того же самого производного типа класса polymorphi c, имеют уникальный макет памяти? - PullRequest
0 голосов
/ 05 мая 2020

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

Layout : при создании объекта некоторого класса C каждый нестатический c элемент данных не ссылочного типа размещается в некоторой части объектного представления C.

Q1: Верно ли это также для объекта (возможно виртуального) базового класса и нестатического c члена данных (возможно виртуального) базового класса?

Q2: Если ответ Q1 верен, этот макет идентичен для каждого объекта самого производного класса ? Например, все ли объекты, созданные из класса C, имеют уникальный макет (например, смещения элементов данных и vtable для виртуальных баз), даже среди нескольких единиц компиляции?

Q3: Если ответ Q2 верен , безопасно ли получать доступ к элементам данных, добавляя смещение к адресу объекта самого производного класса? (Я думаю, это так, потому что приведение нефункционального указателя к 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;
}

1 Ответ

0 голосов
/ 06 мая 2020

По определению, представление объекта содержит все данные, хранящиеся в объекте, включая все нестатические c члены данных и базовые классы, виртуальные или нет. Определение объектного представления в C ++ 17:

последовательность N unsigned char объектов, принимаемых объектом типа * 1009. *, где N равно sizeof(T).

Очевидно, что любые байты, которые используются для хранения подобъектов, «заняты» объектом, поскольку подобъект является частью объект. Это означает, что эти байты являются частью представления объекта. Надеюсь, это ответит на ваш Q1.

Что касается Q2, я не думаю, что стандарт гарантирует, что все полные объекты одного типа будут иметь одинаковый макет, если только тип не является стандартным макетом. На практике я думаю, что было бы необычно найти реализацию, в которой полные объекты одного типа могли бы иметь разные макеты. Если каждая единица перевода видит одно и то же определение класса (что и должно быть, в противном случае вы нарушаете ODR), тогда у компилятора не должно быть проблем с генерацией одного и того же макета в каждой единице перевода, и это кажется разумным. (в противном случае вам может потребоваться создать несколько таблиц). Но, если по какой-то причине реализация действительно хотела изменить макет, я думаю, что она могла бы сделать это даже в пределах одной единицы перевода. разрешено, даже если макет был гарантирован! Если T не является ни стандартным макетом, ни тривиально копируемым типом, то мне неясно, разрешено ли вам вообще выполнять арифметику указателя c внутри объекта типа T, используя char*, который указывает в один из байтов T. Рассмотрим, например, что offsetof гарантированно поддерживается только для типов со стандартной компоновкой, а memcpy вложение объекта в байтовый массив и обратно гарантированно будет четко определено только для тривиально копируемых типов. Скажем, у типа есть виртуальный базовый класс, поэтому он не является ни стандартным, ни легко копируемым. Я не уверен, что этот код четко определен:

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

Если он вообще не определен для вызова foo, то вопрос о том, всегда ли он возвращает одно и то же значение, очевидно, спорный . Возможно, смещения внутри этих типов просто не наблюдаемы (согласно правилам абстрактной машины).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...