Подтвердить что ты не робот

Почему, действительно, удаление неполного типа - это поведение undefined?

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

//in Handle.h file
class Body;

class Handle
{
   public:
      Handle();
      ~Handle() {delete impl_;}
   //....
   private:
      Body *impl_;
};

//---------------------------------------
//in Handle.cpp file

#include "Handle.h"

class Body 
{
  //Non-trivial destructor here
    public:
       ~Body () {//Do a lot of things...}
};

Handle::Handle () : impl_(new Body) {}

//---------------------------------------
//in Handle_user.cpp client code:

#include "Handle.h"

//... in some function... 
{
    Handle handleObj;

    //Do smtg with handleObj...

    //handleObj now reaches end-of-life, and BUM: Undefined behaviour
} 

Я понимаю из стандарта, что это дело направлено в сторону UB, поскольку деструктор Body не является тривиальным. То, что я пытаюсь понять, на самом деле является основной причиной этого.

Я имею в виду, что проблема, похоже, "срабатывает" из-за того, что Handle dtor является встроенным, и поэтому компилятор делает что-то вроде следующего "встроенного расширения" (почти псевдокод здесь).

inline Handle::~Handle()
{
     impl_->~Body();
     operator delete (impl_);
}

Во всех единицах перевода (только Handle_user.cpp в этом случае), где экземпляр Handle может быть уничтожен, не так ли? Я просто не могу этого понять: хорошо, при создании вышеупомянутого встроенного расширения компилятор не имеет полного определения класса Body, но почему он не может просто разрешить компоновщик для объекта impl_->~Body(), и поэтому он вызывает функция деструктора тела, которая фактически определена в файле реализации?

Другими словами: я понимаю, что в момент уничтожения Handle компилятор даже не знает, существует ли (нетривиальный) деструктор или нет для Тела, но почему он не может делать так, как всегда, что оставить "заполнитель" для компоновщика, чтобы заполнить его, и, в конце концов, у него есть "неразрешенный внешний" линкер, если эта функция действительно недоступна?

Я пропустил что-то большое здесь (и в этом случае извините за глупый вопрос)? Если это не так, мне просто интересно понять обоснование этого.

4b9b3361

Ответ 1

Чтобы объединить несколько ответов и добавить свои собственные, без определения класса код вызова не знает:

  • должен ли класс иметь объявленный деструктор, или если используется деструктор по умолчанию, и если это так, то тригр. деструктор по умолчанию является тривиальным,
  • доступен ли деструктор вызывающему коду,
  • какие базовые классы существуют и имеют деструкторы,
  • является ли деструктор виртуальным. Вызываемые виртуальные функции используют другое соглашение о вызове от не виртуальных. Компилятор не может просто "испускать код для вызова" Тело "и оставлять компоновщик для более подробного описания деталей,
  • (это просто, спасибо GMan), перегружен ли delete для класса.

Вы не можете вызвать какую-либо функцию-член по неполному типу по некоторым или всем из этих причин (плюс другая, которая не применяется к деструкторам - вы не знаете параметры или тип возврата). Деструктор ничем не отличается. Поэтому я не уверен, что вы имеете в виду, когда говорите: "Почему он не может так поступать, как всегда?".

Как вы уже знаете, решение состоит в определении деструктора Handle в TU, который имеет определение Body, в том же месте, где вы определяете каждую другую функцию-член Handle, которая вызывает функции или использует данные членов Body. Затем в точке, где delete impl_; скомпилирован, вся информация доступна для испускания кода для этого вызова.

Обратите внимание, что стандарт фактически говорит: 5.3.5/5:

если удаляемый объект неполный тип класса в точке удаление, а полный класс имеет нетривиальный деструктор или функция освобождения, поведение undefined.

Я предполагаю, что это так, что вы можете удалить неполный тип POD, так же, как вы могли бы free его в C. g++ дает вам довольно суровое предупреждение, если вы его попробуете.

Ответ 2

Он не знает, будет ли деструктор публичным или нет.

Ответ 3

Вызов виртуального метода или не виртуального метода - это две совершенно разные вещи.

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

  • поместите все аргументы в стек
  • вызовите функцию и сообщите компоновщику, что она должна разрешить вызов

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

Однако вызов виртуального метода совершенно другой:

  • поместите все аргументы в стек
  • получить vptr экземпляра
  • получить n-й элемент из таблицы vtable
  • вызывать функцию, для которой эта n-я точка входа

Это совершенно по-другому, поэтому компилятор действительно должен знать, вызываете ли вы виртуальный или не виртуальный метод.

Вторая важная вещь заключается в том, что компилятор должен знать, на какой позиции виртуальный метод найден в vtable. Для этого он также должен иметь полное определение класса.

Ответ 4

Без правильного объявления Body код в Handle.h не знает, является ли деструктор virtual или даже доступным (т.е. общедоступным).

Ответ 5

Я просто догадываюсь, но, возможно, это связано с способностью операторов распределения по каждому классу.

То есть:

struct foo
{
    void* operator new(size_t);
    void operator delete(void*);
};

// in another header, like your example

struct foo;

struct bar
{
    bar();
    ~bar() { delete myFoo; }

    foo* myFoo;
};

// in translation unit

#include "bar.h"
#include "foo.h"

bar::bar() :
myFoo(new foo) // uses foo::operator new
{}

// but destructor uses global...!!

И теперь мы не согласовали операторы распределения и ввели поведение undefined. Единственный способ гарантировать, что этого не может произойти, - сказать "сделать тип полным". В противном случае это невозможно гарантировать.

Ответ 6

Это действительно просто частный случай вызова метода (деструктор, косвенно). delete impl_ эффективно просто вызывает деструктор, а затем соответствующий оператор delete (глобальный или класс). Вы не можете вызывать какую-либо другую функцию для неполного типа, поэтому зачем удалять вызов деструктору с особой помощью?

Часть, о которой я не знаю, является тем, что осложнение заставляет стандарт сделать его undefined вместо того, чтобы просто запрещать его, как в вызове метода.