Безопасно ли удалить указатель void?

96

Предположим, у меня есть следующий код:

void* my_alloc (size_t size)
{
   return new char [size];
}

void my_free (void* ptr)
{
   delete [] ptr;
}

Это безопасно? Или нужно ptrпреобразовать в char*перед удалением?

Андрей
источник
Почему вы сами занимаетесь управлением памятью? Какую структуру данных вы создаете? Необходимость в явном управлении памятью в C ++ встречается довольно редко; вам обычно следует использовать классы, которые обрабатывают это за вас из STL (или из Boost в крайнем случае).
Брайан
6
Просто для тех, кто читает, я использую переменные void * в качестве параметров для моих потоков в win c ++ (см. _Beginthreadex). Обычно они указывают на классы.
ryansstack 02
4
В данном случае это универсальная оболочка для нового / удаленного, которая может содержать статистику отслеживания распределения или оптимизированный пул памяти. В других случаях я видел, что указатели на объекты неправильно сохраняются как переменные-члены void * и неправильно удаляются в деструкторе без возврата к соответствующему типу объекта. Так что мне было интересно узнать о безопасности / подводных камнях.
An̲̳̳drew 02
2
Для универсальной оболочки для new / delete вы можете перегрузить операторы new / delete. В зависимости от того, какую среду вы используете, вы, вероятно, получите перехватчики в управлении памятью для отслеживания распределения. Если вы попадаете в ситуацию, когда не знаете, что удаляете, примите это как сильный намек на то, что ваш дизайн неоптимален и нуждается в рефакторинге.
Hans
41
Я думаю, что здесь слишком много вопросов, вместо того чтобы отвечать на него. (Не только здесь, но и во всем ТАК)
Петруза 07

Ответы:

24

Это зависит от «сейфа». Обычно это срабатывает, потому что информация хранится вместе с указателем о самом выделении, поэтому освободитель может вернуть ее в нужное место. В этом смысле это «безопасно», если ваш распределитель использует внутренние граничные теги. (Многие делают.)

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

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

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

Кристофер
источник
9
Как это принятый ответ? Нет смысла «удалять указатель void» - безопасность - спорный вопрос.
Kerrek SB 08
6
«Нет веских причин делать то, что вы делаете, так, как вы это делаете». Это ваше мнение, а не факт.
rxantos 01
8
@rxantos Предоставьте встречный пример, в котором выполнение того, что хочет сделать автор вопроса, является хорошей идеей на C ++.
Кристофер
Я думаю, что этот ответ на самом деле в основном разумный, но я также думаю, что любой ответ на этот вопрос должен хотя бы упомянуть, что это неопределенное поведение.
Kyle Strand
@Christopher Попробуйте написать единую систему сборщика мусора, которая не зависит от типа, а просто работает. Тот факт, что sizeof(T*) == sizeof(U*)для всех T,Uпредполагает, что должна быть возможность иметь void *одну реализацию сборщика мусора , не основанную на шаблонах . Но тогда, когда gc действительно должен удалить / освободить указатель, возникает именно этот вопрос. Чтобы заставить его работать, вам либо понадобятся обертки деструктора лямбда-функции (urgh), либо вам понадобится что-то вроде динамического типа «тип как данные», который позволяет перемещаться между типом и чем-то хранимым.
BitTickler
148

Удаление с помощью указателя void не определено стандартом C ++ - см. Раздел 5.3.5 / 3:

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

И его сноска:

Это означает, что объект нельзя удалить с помощью указателя типа void *, поскольку нет объектов типа void.

.


источник
6
Вы уверены, что выбрали правильную цитату? Я думаю, что сноска относится к этому тексту: «В первой альтернативе (удалить объект), если статический тип операнда отличается от его динамического типа, статический тип должен быть базовым классом динамического типа операнда, а статический тип type должен иметь виртуальный деструктор или поведение не определено. Во второй альтернативе (массив удаления), если динамический тип удаляемого объекта отличается от его статического типа, поведение не определено ». :)
Йоханнес Шауб - лит
2
Вы правы - обновил ответ. Я не думаю, что это отрицает основную мысль?
Нет, конечно нет. Он по-прежнему говорит, что это UB. Более того, теперь он нормативно утверждает, что удаление void * - это UB :)
Йоханнес Шауб - litb
Заполнить указанную адресную память пустого указателя NULLкаким-либо образом изменить управление памятью приложения?
artu-hnrq,
25

Это плохая идея и не то, что вы бы сделали на C ++. Вы без причины теряете информацию о своем типе.

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

Вместо этого вы должны переопределить новый / удалить.

Удаление void *, вероятно, случайно освободит вашу память правильно, но это неправильно, потому что результаты не определены.

Если по какой-то неизвестной мне причине вам нужно сохранить указатель в void *, то освободите его, вы должны использовать malloc и free.

Брайан Р. Бонди
источник
5
Вы правы в том, что деструктор не вызывается, но ошибаетесь в том, что размер неизвестен. Если вы даете удалить указатель , который вы получили от нового, он делает на самом деле знает размер вещи удаляется, полностью независимо от типа. Как это происходит, не указано в стандарте C ++, но я видел реализации, в которых размер сохранялся непосредственно перед данными, на которые указывает указатель, возвращаемый 'new'.
KeyserSoze 02
Удалена часть о размере, хотя стандарт C ++ говорит, что он не определен. Я знаю, что malloc / free подойдет для указателей void *.
Брайан Р. Бонди,
Не думаете, что у вас есть ссылка на соответствующий раздел стандарта? Я знаю, что несколько реализаций new / delete, на которые я смотрел, определенно работают правильно без знания типов, но я признаю, что не смотрел на то, что определяет стандарт. IIRC C ++ изначально требовал подсчета элементов массива при удалении массивов, но больше не требует в новейших версиях.
KeyserSoze 02
См. Ответ @Neil Butterworth. На мой взгляд, его ответ должен быть принятым.
Брайан Р. Бонди,
2
@keysersoze: В целом я не согласен с вашим утверждением. Тот факт, что какая-то реализация сохраняла размер до выделения памяти, не означает, что это правило.
13

Удаление указателя void опасно, потому что деструкторы не будут вызываться для значения, на которое он фактически указывает. Это может привести к утечке памяти / ресурсов в вашем приложении.

ДжаредПар
источник
2
char не имеет конструктора / деструктора.
rxantos
9

Вопрос не имеет смысла. Отчасти ваше замешательство может быть связано с небрежным языком, который люди часто используют с delete:

Вы используете deleteдля уничтожения объекта, который был размещен динамически. Сделайте это, вы создадите выражение удаления с указателем на этот объект . Вы никогда не «удаляете указатель». На самом деле вы «удаляете объект, который идентифицируется по его адресу».

Теперь мы видим, почему вопрос не имеет смысла: указатель void - это не «адрес объекта». Это просто адрес без какой-либо семантики. Он мог быть получен из адреса реального объекта, но эта информация потеряна, поскольку была закодирована в типе исходного указателя. Единственный способ восстановить указатель на объект - вернуть указатель void обратно к указателю на объект (что требует, чтобы автор знал, что означает указатель). voidсам по себе является неполным типом и, следовательно, никогда не является типом объекта, а указатель void никогда не может использоваться для идентификации объекта. (Объекты идентифицируются вместе по их типу и адресу.)

Керрек С.Б.
источник
По общему признанию, этот вопрос не имеет особого смысла без какого-либо окружающего контекста. Некоторые компиляторы C ++ по-прежнему с радостью компилируют такой бессмысленный код (если они чувствуют себя полезными, могут выдать предупреждение об этом). Итак, вопрос был задан, чтобы оценить известные риски запуска устаревшего кода, который содержит эту опрометчивую операцию: произойдет ли сбой? утечка некоторой или всей памяти массива символов? что-то еще, зависящее от платформы?
An̲̳̳drew 01
Спасибо за вдумчивый ответ. Голосование за!
An̲̳̳drew 01
1
@Andrew: Боюсь, что стандарт довольно ясен по этому поводу: «Значение операнда deleteможет быть значением нулевого указателя, указателем на объект, не являющийся массивом, созданный предыдущим выражением new , или указателем на подобъект, представляющий базовый класс такого объекта. В противном случае поведение не определено. " Так что, если компилятор принимает ваш код без диагностики, это не что иное, как ошибка в компиляторе ...
Керрек С.Б.
1
@KerrekSB - Re это не что иное, как ошибка в компиляторе - я не согласен. Стандарт говорит, что поведение не определено. Это означает, что компилятор / реализация может делать что угодно и при этом соответствовать стандарту. Если в ответ компилятор говорится, что вы не можете удалить указатель void *, это нормально. Если ответ компилятора - очистить жесткий диск, это тоже нормально. OTOH, если ответ компилятора состоит в том, чтобы не генерировать диагностику, а вместо этого генерировать код, который освобождает память, связанную с этим указателем, это тоже нормально. Это простой способ справиться с этой формой УБ.
Дэвид Хаммен
Просто добавлю: я не одобряю использование delete void_pointer. Это неопределенное поведение. Программисты никогда не должны вызывать неопределенное поведение, даже если кажется, что ответ делает то, что хотел программист.
Дэвид Хаммен
7

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

void* my_alloc (size_t size)
{
   return ::operator new(size);
}

void my_free (void* ptr)
{
   ::operator delete(ptr);
}

Обратите внимание, что, в отличие от malloc(), operator newвыдает std::bad_allocошибку (или вызывает, new_handlerесли он зарегистрирован).

bk1e
источник
Это правильно, поскольку у char нет конструктора / деструктора.
rxantos
5

Потому что у char нет специальной логики деструктора. ЭТО не сработает.

class foo
{
   ~foo() { printf("huzza"); }
}

main()
{
   foo * myFoo = new foo();
   delete ((void*)foo);
}

В d'ctor не позвонят.


источник
5

Если вы хотите использовать void *, почему бы вам не использовать только malloc / free? new / delete - это больше, чем просто управление памятью. По сути, new / delete вызывает конструктор / деструктор, и это еще не все. Если вы просто используете встроенные типы (например, char *) и удалите их через void *, это сработает, но все же не рекомендуется. Суть в том, чтобы использовать malloc / free, если вы хотите использовать void *. В противном случае вы можете использовать функции шаблона для вашего удобства.

template<typename T>
T* my_alloc (size_t size)
{
   return new T [size];
}

template<typename T>
void my_free (T* ptr)
{
   delete [] ptr;
}

int main(void)
{
    char* pChar = my_alloc<char>(10);
    my_free(pChar);
}
молодой
источник
Я не писал код в примере - наткнулся на этот шаблон, который используется в паре мест, любопытно смешивая управление памятью C / C ++, и мне стало интересно, в чем заключаются конкретные опасности.
An̲̳̳drew 02
Написание C / C ++ - это рецепт неудачи. Кто бы это ни написал, должен был написать то или другое.
Дэвид Торнли
2
@David Это C ++, а не C / C ++. C не имеет шаблонов, а также не использует новые и удаляемые.
rxantos
5

Многие люди уже прокомментировали, что нет, удалять указатель void небезопасно. Я согласен с этим, но я также хотел добавить, что если вы работаете с указателями void для выделения смежных массивов или чего-то подобного, вы можете сделать это, newчтобы вы могли deleteбезопасно использовать (с, кхм , немного лишних работ). Это делается путем выделения пустого указателя на область памяти (называемую «ареной»), а затем передачи указателя на арену в new. См. Этот раздел в FAQ по C ++ . Это общий подход к реализации пулов памяти в C ++.

Пол Мори
источник
0

Вряд ли есть причина для этого.

Прежде всего, если вы не знаете тип данных, и все, что вы знаете, это void*то, что вам действительно следует рассматривать эти данные как бесполезный двоичный объект данных ( unsigned char*) и использовать malloc/ freeдля работы с ним. . Иногда это требуется для таких вещей, как данные формы сигнала и тому подобное, когда вам нужно передать void*указатели на C apis. Хорошо.

Если вы делаете знать тип данных (т.е. имеет CTOR / dtor), но по какой - то причине вы закончили с void*указателем (по какой - либо причине у вас есть) , то вы действительно должны бросить его обратно к типу вы знаете его быть , и взывать deleteк нему.

бобобобо
источник
0

Я использовал void * (также известный как неизвестные типы) в своей структуре для отражения кода и других подвигов неоднозначности, и до сих пор у меня не было проблем (утечка памяти, нарушения доступа и т. Д.) С любыми компиляторами. Только предупреждения о нестандартной работе.

Совершенно имеет смысл удалить неизвестное (void *). Просто убедитесь, что указатель соответствует этим рекомендациям, иначе он может перестать иметь смысл:

1) Неизвестный указатель не должен указывать на тип, который имеет тривиальный деконструктор, и поэтому, когда он приведен как неизвестный указатель, он НИКОГДА НЕ УДАЛЯЕТСЯ. Удалите неизвестный указатель только ПОСЛЕ приведения его обратно в исходный тип.

2) На экземпляр ссылается как на неизвестный указатель в памяти с привязкой к стеку или кучей? Если неизвестный указатель ссылается на экземпляр в стеке, он НИКОГДА НЕ УДАЛЯЕТСЯ!

3) Вы на 100% уверены, что неизвестный указатель является допустимой областью памяти? Нет, это НИКОГДА НЕ УДАЛЯЕТСЯ!

В целом, очень мало прямой работы, которую можно выполнить с использованием неизвестного типа указателя (void *). Однако косвенно void * является отличным преимуществом для разработчиков C ++, когда требуется неоднозначность данных.

zackery.fix
источник
0

Если вам нужен только буфер, используйте malloc / free. Если вы должны использовать new / delete, рассмотрите тривиальный класс-оболочку:

template<int size_ > struct size_buffer { 
  char data_[ size_]; 
  operator void*() { return (void*)&data_; }
};

typedef sized_buffer<100> OpaqueBuffer; // logical description of your sized buffer

OpaqueBuffer* ptr = new OpaqueBuffer();

delete ptr;
Санджая Р.
источник
0

В частном случае char.

char - это внутренний тип, не имеющий специального деструктора. Так что аргументы в пользу утечки спорны.

sizeof (char) обычно равен единице, поэтому аргумента выравнивания тоже нет. В случае редкой платформы, где sizeof (char) не один, они выделяют память, достаточно выровненную для своего char. Так что аргумент о мировоззрении тоже спорный.

В этом случае malloc / free будет быстрее. Но вы теряете std :: bad_alloc и должны проверять результат malloc. Вызов глобальных операторов new и delete может быть лучше, поскольку это позволяет обходить посредника.

rxantos
источник
1
" sizeof (char) обычно равно одному " sizeof (char) ВСЕГДА один
curiousguy
Лишь недавно (2019) люди думали, что на newсамом деле определено бросить. Это неправда. Это зависит от компилятора и переключателя компилятора. См., Например, /GX[-] enable C++ EH (same as /EHsc)переключатели MSVC2019 . Также во встроенных системах многие предпочитают не платить налог на производительность за исключения C ++. Поэтому предложение, начинающееся с «Но вы лишаетесь std :: bad_alloc ...», вызывает сомнения.
BitTickler