Разрешено ли писать экземпляр Derived поверх экземпляра Base? - PullRequest
3 голосов
/ 02 февраля 2012

Скажите, код

    class Derived: public Base {....}

    Base* b_ptr = new( malloc(sizeof(Derived)) ) Base(1);
    b_ptr->f(2);
    Derived* d_ptr = new(b_ptr) Derived(3);
    b_ptr->g(4);
    d_ptr->f(5);

кажется разумным, и LSP удовлетворен.

Я подозреваю, что этот код стандартно разрешен, когда Base и Derived являются POD, и запрещен в противном случае (потому что vtbl ptr перезаписывается). Первая часть моего вопроса такова: пожалуйста, укажите точное условие такой перезаписи.

Могут существовать другие стандартные способы перезаписи.

Вторая часть моего вопроса: есть ли другие способы? Каковы их точные предпосылки?

ОБНОВЛЕНИЕ: Я НЕ хочу писать такой код; Меня интересует теоретическая возможность (или невозможность) такого кода. Итак, это «стандартный нацистский» вопрос, а не вопрос «как я могу ...». (Мой вопрос нужно перенести на другой сайт stackoverflow?)

ОБНОВЛЕНИЕ2 & 4: А как насчет деструкторов? Предполагаемая семантика этого кода: «Базовый экземпляр (деструктивно) обновляется с помощью фрагмента производного экземпляра». Для простоты предположим, что у базового класса есть тривиальный деструктор.

ОБНОВЛЕНИЕ3: Самое интересное для меня - действительность доступа через b_ptr->g(4)

Ответы [ 4 ]

8 голосов
/ 02 февраля 2012

Вам действительно нужно сделать b_ptr = d_ptr после размещения нового из Derived, если подобъект Base не является первым в макете Derived. Как написано, b_ptr->g(4) вызывает неопределенное поведение.

Правило (3,8 basic.life):

Если по истечении времени жизни объекта и до повторного использования или освобождения хранилища, которое занимал объект, в месте хранения, которое занимал исходный объект, создается новый указатель, который указывает на исходный объект , ссылка, которая ссылается на исходный объект, или имя исходного объекта будет автоматически ссылаться на новый объект и, как только начнется время жизни нового объекта, может использоваться для манипулирования новым объектом, если :

  • хранилище для нового объекта точно перекрывает место хранения, которое занимал исходный объект, и
  • новый объект того же типа, что и исходный объект (игнорируя квалификаторы cv верхнего уровня), и
  • тип исходного объекта не является const-квалифицированным, и, если тип класса, не содержит нестатических элемент данных, тип которого является постоянным или ссылочным, и
  • исходный объект был наиболее производным объектом (1.8) типа T, а новый объект - наиболее производным объектом типа T (то есть они не являются подобъектами базового класса ) .

Вам также, вероятно, следует уничтожить старый объект перед повторным использованием его памяти, но стандарт этого не требует. Однако, если это не будет сделано, произойдет утечка любых ресурсов, принадлежащих старому объекту. Полное правило приведено в разделе 3.8 (basic.life) Стандарта:

Программа может завершить время жизни любого объекта, повторно используя хранилище, которое занимает объект, или явно вызывая деструктор для объекта типа класса с нетривиальным деструктором. Для объекта типа класса с нетривиальным деструктором, программе не требуется явно вызывать деструктор до повторного использования или освобождения хранилища, которое занимает объект; однако, если нет явного вызова деструктор или если выражение delete (5.3.5) не используется для освобождения хранилища, деструктор не должен вызываться неявно, и любая программа, зависящая от побочных эффектов, создаваемых деструктором, имеет неопределенное поведение .

1 голос
/ 02 февраля 2012

Если вы напишите этот код более четко, вам будет легче увидеть, что идет не так:

void * addr = std::malloc(LARGE_NUMBER);

Base * b = new (addr) Base;
b->foo();                    // no problem

Derived * d = new (addr) Derived;
d->bar();                    // also fine  (#1)

b->foo();                    // Error! b no longer points to a Base!

static_cast<Base*>(d)->foo(); // OK
b = d; b->foo();              // also OK

Проблема в том, что в строке, отмеченной (# 1), b и d точкана совершенно разные, не связанные между собой вещи, и поскольку вы перезаписали память прежнего объекта *b, b фактически больше не действует вообще.

У вас могут возникнуть некоторые ошибочные мысли о Base* иDerived* являются конвертируемыми типами указателей, но это не имеет ничего общего с существующей ситуацией, и ради этого примера оба типа также могут быть совершенно не связаны между собой.Только в одной из последних двух строк мы используем тот факт, что Derived* конвертируется в Base*, когда мы выполняем фактическое преобразование.Но обратите внимание, что это преобразование является подлинным значением преобразования, и d - это не тот же указатель , что и static_cast<Base*>(d) (по крайней мере, в том, что касается языка).

Наконец, давайте исправим этот беспорядок:

d->~Derived();
std::free(addr);

Возможность уничтожить оригинал *b упущена, поэтому мы могли это просочиться.

1 голос
/ 02 февраля 2012

Я думаю, что перезапись разрешена.

Если бы это был я, я бы, вероятно, позвонил бы Base::~Base перед повторным использованием хранилища, чтобы время жизни исходного объекта закончилось без ошибок.Но стандарт явно позволяет вам повторно использовать хранилище без вызова деструктора.

Я не верю, что ваш доступ через b_ptr действителен.Время жизни базового объекта закончилось.

(см. 3.8 / 4 в любом из стандартов для правил жизни).

И я также не совсем убежден, что b_ptr должен давать то же самоеадрес, который был первоначально возвращен вызовом malloc ().

1 голос
/ 02 февраля 2012

Вы инициализируете один и тот же фрагмент памяти дважды. Это не закончится хорошо.

Предположим, например, что конструктор Base выделяет некоторую память и сохраняет ее в указателе. Во второй раз через конструктор первый указатель будет перезаписан, а память просочилась.

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