Является указателем на базу всегда <= указатель на производный класс?

Интересно, гарантировало ли это стандартом С++ однократное наследование, чтобы объект "расти" вверх, которому даны class Base и class Derived: public Base, а указатель Derived* ptr, результат dynamic_cast<Base*>(ptr) будет всегда будет численно меньше или равно ptr.

Ответ 1

Нет. Макет памяти - это детализация реализации.

При этом мне не известно о какой-либо реализации, которая на самом деле этого не делает, если нет виртуальных функций. Если вы вводите виртуальную функцию в Derived (но ее не было в Base), указатель виртуальной таблицы мог (в зависимости от реализации) располагаться перед полями Base (делая Base* больше, чем Derived*).

Разъяснение:

Пример выше - это Visual С++. Вы можете проверить это, используя следующий код:

class Base {
    int X;
};

class Derived : public Base {
    virtual void f() {
    }
    int Y;
};

int main() {

    Derived d;
    Derived* d_ptr = &d;
    Base* b_ptr = dynamic_cast<Base*>(d_ptr); // static_cast would be enough BTW.

    bool base_smaller_or_equal = (ptrdiff_t)b_ptr <= (ptrdiff_t)d_ptr;

    return 0;

}

В Visual С++ false будет false. Судя по комментарию @enobayram, он должен быть true под GCC.

В любом случае это деталь реализации, и на нее нельзя полагаться.

Ответ 2

Нет.

Однако пока не нужно паниковать. Этот вопрос, хотя и не полностью рассмотренный в стандарте С++, отвечает за документ ABI, за которым последует компилятор. Многие компиляторы, такие как gcc, Clang или icc (но не VС++), следуют Itanium ABI.

В Itanium ABI, если у вас есть одно не виртуальное наследование, а базовый класс имеет виртуальный метод, то Derived и Base всегда будут иметь одинаковый адрес.

Это, как говорится, это реализация, о которой вам действительно не нужно беспокоиться. Вы можете отлично писать С++ стандартно совместимый код и по-прежнему управлять своими вариантами использования. Дело в том, что С++ позволяет использовать любой указатель на void* и char* (последний как конкретное исключение для сглаживания) и обратно. Единственное, о чем вам нужно беспокоиться, это то, что когда вы делаете a Base* до void*, вам нужно вернуть его обратно в Base*, а не в Derived*. То есть тип, который вы вставляете, и тип, который вы возвращаете, должны совпадать.

Намного сложнее, однако, знать (точно) размер объекта. Это требует применения sizeof к текущему динамическому типу объекта, и нет возможности получить его virtually.

Мой совет будет состоять в том, чтобы на самом деле измерить ваш базовый класс:

class Base {
public:
  char const* address() const { return (char const*)dynamic_cast<void const*>(this); }
  size_t offset() const { return this->address() - (char const*)this; }

  virtual size_t size() const { return sizeof(Base); } // to be overriden

  virtual ~Base() {}
};

Это помогает получить всю необходимую информацию (см. демонстрацию). Обратите внимание, что с Itanium ABI offset() всегда будет возвращаться 0, но по крайней мере вы здесь не предполагаете.