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

Полагайте, что этот классический пример раньше объяснял, что не сделать с предописаниями:

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

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

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

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

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

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

25
задан Sam 12 August 2014 в 09:22
поделиться

6 ответов

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

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

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

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

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

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

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

27
ответ дан 28 November 2019 в 21:05
поделиться

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

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

1
ответ дан 28 November 2019 в 21:05
поделиться

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

3
ответ дан 28 November 2019 в 21:05
поделиться

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

То есть:

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

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

2
ответ дан 28 November 2019 в 21:05
поделиться

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

6
ответ дан 28 November 2019 в 21:05
поделиться

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

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

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

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

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

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

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

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

5
ответ дан 28 November 2019 в 21:05
поделиться
Другие вопросы по тегам:

Похожие вопросы: