Безопасно ли сделать деструктор не виртуальным и удалить указатель базы в особых случаях? - PullRequest
1 голос
/ 15 апреля 2019

Допустим, у нас есть класс BST_Node:

struct BST_Node {
  BST_Node* left;
  BST_Node* right;
}

И класс AVL_Node:

struct AVL_Node : BST_Node {
  int height;
}

и в некоторых функциях

void destroyTree() {
  BST_Node *mynode = new AVL_Node;
  delete mynode; //  Is it ok ?
}

Вопрос № 1

Когда деструктор не является виртуальным, но в производных есть только типы примитивов, безопасно ли вызывать delete для базового класса? (не будет ли утечек памяти?)

Вопрос № 2

Каково правило при объявлении виртуального деструктора в производном классе только ? Как я понял, все деструкторы - это одна и та же функция, мы можем назвать ее destructor (), а затем, когда мы удаляем базовый указатель, деструктор вызывается только для базового класса, но при удалении производного класса деструктор также будет отправляется в дочерние классы.

Ответы [ 2 ]

0 голосов
/ 15 апреля 2019

Беги

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

Теперь во время выполнения каждый компилятор превращает delete foo в «найти код деструктора, запустить его, затем очистить память». Но вы не можете основывать свое понимание того, что означает код C ++ на основе кода времени выполнения, испускаемого компилятором.

Так что вы наивно можете подумать: «Мне все равно, если мы запустим неправильный код уничтожения; единственное, что я добавил, это int. А код очистки памяти обрабатывает перераспределение. Так что мы хороши!»

Вы даже идете и тестируете его, смотрите на произведенную сборку, и все работает! И вы заключаете, что здесь нет проблем.

Вы не правы.

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

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

Что означает ваша программа C ++ в «абстрактной машине», стандарт C ++ определяет , значение . Именно в этой абстрактной машине происходят оптимизации и преобразования кода. Знание того, как на вашем физическом компьютере генерируется изолированный фрагмент кода, не говорит вам, что делает этот фрагмент кода.

Вот конкретный пример:

struct Foo {};
struct Bar:Foo{};

Foo* do_something( bool cond1, bool cond2 ) {
  Foo* foo = nullptr;
  if (cond1)
    foo = new Bar;
  else
    foo = new Foo;

  if (cond2 && !cond1)
    inline_code_to_delete_user_folder();

  if (cond2) {
    delete foo;
    foo = nullptr;
  }
  return foo;
}

вот игрушка с некоторыми типами игрушек.

В нем мы создаем указатель на Bar или Foo на основе cond1.

Тогда мы можем сделать что-то опасное.

Наконец, если cond2 истинно, мы очищаем Foo* foo.

Дело в том, что если мы называем delete foo и foo не является Foo, это неопределенное поведение. Компилятор может обоснованно обосновать: «Хорошо, мы вызываем delete foo, поэтому *foo является объектом типа Foo».

Но если foo является указателем на фактический Foo, то, очевидно, cond1 должно быть ложным , потому что только когда оно ложно, foo указывает на фактическое Foo .

Таким образом, логически, cond2 истинно, подразумевает cond1 верно. Всегда. Везде. Заднее число.

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

Foo* do_something( bool cond1, bool cond2 ) {
  if (cond2) {
    Foo* foo = new Foo;
    inline_code_to_delete_user_folder();
    delete foo;
    return nullptr;
  }       
  Foo* foo = nullptr;
  if (cond1)
    foo = new Bar;
  else
    foo = new Foo;

  return foo;
}

что довольно опасно, не правда ли? Мы просто отказались от проверки cond1 и удаляли пользовательскую папку всякий раз, когда вы передавали true в cond2.

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

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

Беги

0 голосов
/ 15 апреля 2019

Когда деструктор не является виртуальным, но в производных есть только типы примитивов, безопасно ли вызывать delete для базового класса?(не будет ли утечек памяти?)

Вы можете не знать об этом, но это два разных вопроса.

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

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

Когда вы пишете код, подобный delete mynode;, компилятор должен выяснить, какой деструктор вызвать.Если деструктор для mynode не является виртуальным, то он всегда будет использовать базовый деструктор, делая то, что должен делать базовый деструктор, но не то, что должен делать производный деструктор.

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

Но если ваш кодвместо этого они выглядят так:

struct AVL_Node : public BST_Node {
    std::unique_ptr<int> height = std::make_unique<int>();
};

Тогда этот код определенно вызовет утечки памяти, даже если мы явно использовали умный указатель при создании производного объекта!Умный указатель не спасает нас от проблем с delete использованием базового указателя с деструктором, отличным от virtual.

И вообще, ваш код может вызвать любую утечку, включая, но неограничено утечками ресурсов, утечками дескрипторов файлов и т. д., если AVL_Node были ответственны за другие объекты.Рассмотрим, к примеру, если бы у AVL_Node было что-то подобное, что чрезвычайно часто встречается в некоторых видах графического кода:

struct AVL_Node : public BST_Node {
    int handle;
    AVL_Node() {
        glGenArrays(1, &handle);
    }
    /*
     * Pretend we implemented the copy/move constructors/assignment operators as needed
     */
    ~AVLNode() {
        glDeleteArrays(1, &handle);
    }
};

Ваш код не пропустит память (в вашемсобственного кода), но при этом будет протекать объект OpenGL (и любая память, выделенная этим объектом).

Каково правило при объявлении виртуального деструктора только в производном классе?

Если вы никогда не планируете хранить указатель на базовый класс, тогда это нормально.

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

Итак, вот пример, который мы будем использовать для ясности:

struct A {
    std::unique_ptr<int> int_ptr = std::make_unique<int>();
};

struct B : A {
    std::unique_ptr<int> int_ptr_2 = std::make_unique<int>();
    virtual ~B() = default;
};

struct C : B {
    std::unique_ptr<int> int_ptr_3 = std::make_unique<int>();
    //virtual ~C() = default; // Unnecessary; implied by B having a virtual destructor
};

Теперь вот весь код, который безопасно и небезопасно использовать с этими тремя классами:

auto a1 = std::make_unique<A>(); //Safe; a1 knows its own type
std::unique_ptr<A> a2 = std::make_unique<A>(); //Safe; exactly the same as a1
auto b1 = std::make_unique<B>(); //Safe; b1 knows its own type
std::unique_ptr<B> b2 = std::make_unique<B>(); //Safe; exactly the same as b1
std::unique_ptr<A> b3 = std::make_unique<B>(); //UNSAFE: A does not have a virtual destructor!
auto c1 = std::make_unique<C>(); //Safe; c1 knows its own type
std::unique_ptr<C> c2 = std::make_unique<C>(); //Safe; exactly the same as c1
std::unique_ptr<B> c3 = std::make_unique<C>(); //Safe; B has a virtual destructor
std::unique_ptr<A> c4 = std::make_unique<C>(); //UNSAFE: A does not have a virtual destructor!

Итакесли B (класс с деструктором virtual) наследует от A (класс без деструктора virtual), но как программист, вы обещаете, что никогда не будете ссылаться на экземпляр B сA указатель, то у вас нето чем беспокоитьсяТаким образом, в этом случае, как показано в моем примере, могут быть веские причины для объявления деструктора производного класса virtual, оставляя деструктор суперкласса не-1062 *.

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