Можете ли вы вызвать деструктор без вызова конструктора? - PullRequest
1 голос
/ 04 июля 2019

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

Это то, что я пробежал:

#include <iostream>

struct test
{
    int num = 3;

    test() { std::cout << "Init\n"; }
    ~test() { std::cout << "Destroyed: " << num << "\n"; }
};

int main()
{
    test* array = (test*)malloc(3 * sizeof(test));

    for (int i = 0; i < 3; i += 1)
    {
        std::cout << array[i].num << "\n";
        array[i].num = i;
        //new(array + i) i; placement new is not being used
        std::cout << array[i].num << "\n";
    }

    for (int i = 0; i < 3; i += 1)
    {
        (array + i)->~test();
    }

    free(array);

    return 0;
}

Какие выходы:

0 ->- 0
0 ->- 1
0 ->- 2
Destroyed: 0
Destroyed: 1
Destroyed: 2

Несмотря на то, что не построены индексы массива. Это "здоровый"? То есть я могу просто обращаться с деструктором как с «просто функцией»? (помимо того факта, что деструктор неявно знает, где расположены элементы данных относительно указанного мной указателя)

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

(сноска: причина, по которой я не хочу использовать конструкторы, заключается в том, что во многих случаях память просто не нужно инициализировать и это происходит медленно)

Ответы [ 4 ]

6 голосов
/ 04 июля 2019

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

Вероятно, в вашем примере это "похоже" ведет себя правильно, потому что ваша структура тривиальна (int :: ~ int - это неоперация).

Вы также теряете память (деструкторы уничтожают данный объект, но исходная память, выделенная с помощью malloc, все еще должна быть free d).

Редактировать: Возможно, вы захотите посмотреть и на этот вопрос , так как это очень похожая ситуация, просто используя распределение стека вместо malloc. Это дает некоторые фактические цитаты из стандарта вокруг времени жизни и конструкции объекта.

Я также добавлю это: в случае, если вы не используете размещение new и оно явно требуется (например, struct содержит некоторый класс контейнера или vtable и т. Д.), Вы столкнетесь с реальными проблемами. В этом случае пропуск нового вызова с размещением почти наверняка даст вам 0 выигрыш в производительности для очень хрупкого кода - в любом случае, это просто не очень хорошая идея.

2 голосов
/ 04 июля 2019

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

Таким образом, правило таково: Если вы не инициализировали память как определенный тип, вы не можете интерпретировать и использовать эту память как объектэтот тип;в противном случае это неопределенное поведение. char и unsigned char в качестве исключений).

Давайте построчно проанализируем ваш код.

test* array = (test*)malloc(3 * sizeof(test));

ЭтоСтрока инициализирует скалярный указатель array, используя адрес памяти, предоставленный системой.Обратите внимание, что память не инициализируется для любого типа .Это означает, что вы не должны обрабатывать эту память как любой объект (даже как скаляры, подобные int, оставьте в стороне ваш test тип класса).

Позже вы написали:

std::cout << array[i].num << "\n";

Используется память как тип test, что нарушает указанное выше правило, приводя к неопределенному поведению.

И позже:

(array + i)->~test();

Вы использовали памятьtest введите снова!Вызов деструктора также использует объект!Это также UB.

В вашем случае вам повезло, что ничего плохого не происходит, и вы получаете что-то разумное.Однако UB зависят исключительно от реализации вашего компилятора.Он может даже решить отформатировать ваш диск, и это все еще соответствует стандартам.

1 голос
/ 04 июля 2019

То есть я могу просто трактовать деструктор как "просто функцию"?

Нет. Хотя он во многом похож на другие функции, у деструктора есть свои особенности. Они сводятся к шаблону, похожему на ручное управление памятью. Так же, как распределение и освобождение памяти должно происходить парами, так и строительство и разрушение. Если вы пропустите один, пропустите другой. Если вы звоните одному, звоните другому. Если вы настаиваете на ручном управлении памятью, инструментами для построения и уничтожения являются размещение нового и явный вызов деструктора. (Код, использующий new и delete, объединяет выделение и построение в один шаг, а уничтожение и освобождение объединяются в другой.)

Не пропускайте конструктор для объекта, который будет использоваться. Это неопределенное поведение. Кроме того, чем менее тривиален конструктор, тем больше вероятность, что что-то пойдет не так, если вы пропустите это. То есть, чем больше вы экономите, тем больше ломаетесь. Пропуск конструктора для используемого объекта не является способом повышения эффективности & mdash; это способ написания неработающего кода. Неэффективный, правильный код превосходит эффективный код, который не работает.

Один кусочек разочарования: такого рода низкоуровневое управление может стать большим вложением времени. Пройдите этот путь только в том случае, если есть реальная вероятность окупаемости. Не усложняйте свой код оптимизацией просто ради оптимизации. Также рассмотрите более простые альтернативы, которые могут получить аналогичные результаты с меньшими накладными расходами кода. Возможно, конструктор, который не выполняет никаких инициализаций, кроме как пометить объект как не инициализированный? (Детали и возможности зависят от участвующего класса, поэтому выходят за рамки этого вопроса.)

Один кусочек ободрения: Если вы думаете о стандартной библиотеке, вы должны понимать, что ваша цель достижима. Я бы представил vector::reserve в качестве примера чего-то, что может выделить память без ее инициализации.

0 голосов
/ 04 июля 2019

У вас есть UB для доступа к полю из несуществующего объекта.

Вы можете оставить поле неинициализированным, выполнив noop конструктора. компилятор может легко выполнить инициализацию, например:

struct test
{
    int num; // no = 3

    test() { std::cout << "Init\n"; } // num not initalized
    ~test() { std::cout << "Destroyed: " << num << "\n"; }
};

Демо

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

struct uninitialized_tag {};

struct uninitializable_int
{
    uninitializable_int(uninitialized_tag) {} // No initalization
    uninitializable_int(int num) : num(num) {}

    int num;
};

Демо

...