Почему на самом деле удаление неполного типа является неопределенным поведением? - PullRequest
25 голосов
/ 25 марта 2010

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

//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 не тривиален. То, что я пытаюсь понять, действительно является коренной причиной этого.

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

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

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

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

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

Ответы [ 6 ]

26 голосов
/ 25 марта 2010

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

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

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

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

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

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

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

6 голосов
/ 25 марта 2010

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

5 голосов
/ 25 марта 2010

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

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

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

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

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

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

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

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

3 голосов
/ 25 марта 2010

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

2 голосов
/ 25 марта 2010

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

То есть:

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...!!

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

1 голос
/ 25 марта 2010

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

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

...