Действительно, почему удаление неполного типа является неопределённым поведением?

StackOverflow https://stackoverflow.com/questions/2517245

Вопрос

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

//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, которая фактически определена в его файле реализации?

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

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

Это было полезно?

Решение

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

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

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

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

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

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

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

Другие советы

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

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

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

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

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

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

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

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

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

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

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

То есть:

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top