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

#c #memory-leaks #shared-ptr #make-shared

#c #управление памятью #Кастинг #указатели void

Вопрос:

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

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

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

Безопасно ли это? Или его необходимо ptr преобразовать в char* перед удалением?

Комментарии:

1. Почему вы сами занимаетесь управлением памятью? Какую структуру данных вы создаете? Необходимость в явном управлении памятью довольно редка в C ; обычно вам следует использовать классы, которые обрабатывают это за вас из STL (или, в крайнем случае, из Boost).

2. Просто для тех, кто читает, я использую переменные void * в качестве параметров для моих потоков в win c ( см. _beginthreadex ). Обычно они фактически указывают на классы.

3. В данном случае это оболочка общего назначения для создания / удаления, которая может содержать статистику отслеживания распределения или оптимизированный пул памяти. В других случаях я видел, что указатели на объекты неправильно сохранялись как переменные-члены void * и неправильно удалялись в деструкторе без возврата к соответствующему типу объекта. Итак, мне было любопытно узнать о безопасности / подводных камнях.

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

5. Я думаю, что слишком много вопросов ставится под сомнение вместо ответа на него. (Не только здесь, но и во всех SO)

Ответ №1:

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

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

И сноска к нему:

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

.

Комментарии:

1. Нет, конечно, нет. Он по-прежнему говорит, что это UB. Более того, теперь в нем нормативно указано, что удаление void * является UB 🙂

2. Заполнение памяти с указанным адресом указателя void NULL имеет какое-либо значение для управления памятью приложения?

3. Ответ на этот вопрос был получен в 2009 году, это все еще верно по состоянию на C 17/20?

Ответ №2:

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

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

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

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

Комментарии:

1. Насколько это приемлемый ответ? «Удалять указатель void» не имеет никакого смысла — безопасность является спорным вопросом.

2. «Нет веской причины делать то, что вы делаете, так, как вы это делаете». Это ваше мнение, а не факт.

3. @rxantos приводит встречный пример, в котором выполнение того, что хочет сделать автор вопроса, является хорошей идеей в C .

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

5. @Christopher Попробуйте написать единую систему сбора мусора, которая не зависит от типа, а просто работает. Тот факт, что sizeof(T*) == sizeof(U*) для всех T,U , предполагает, что должно быть возможно иметь 1 реализацию сборщика мусора без шаблонов, void * основанную на сборке мусора. Но затем, когда gc действительно должен удалить / освободить указатель, возникает именно этот вопрос. Чтобы это заработало, вам либо понадобятся оболочки деструктора лямбда-функции (urgh), либо вам понадобится какая-то динамическая штука типа «тип как данные», которая позволяет переключаться между типом и чем-то, что можно хранить.

Ответ №3:

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

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

Вместо этого вам следует переопределить new / delete .

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

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

Комментарии:

1. Вы правы в том, что деструктор не вызывается, но ошибаетесь в том, что размер неизвестен. Если вы даете delete указатель, полученный из new , он действительно знает размер удаляемого объекта, полностью независимо от типа. Как это делается, не указано стандартом C , но я видел реализации, в которых размер сохраняется непосредственно перед данными, на которые указывает указатель, возвращаемый ‘new’.

2. Удалена часть о размере, хотя стандарт C гласит, что он не определен. Я знаю, что malloc / free будет работать, хотя для указателей void * .

3. Не думаю, что у вас есть веб-ссылка на соответствующий раздел стандарта? Я знаю, что несколько рассмотренных мной реализаций new / delete определенно работают корректно без знания типа, но я признаю, что я не смотрел на то, что определяет стандарт. Изначально IIRC C требовал подсчета элементов массива при удалении массивов, но в новейших версиях это больше не требуется.

4. Пожалуйста, смотрите ответ @Neil Butterworth. На мой взгляд, его ответ должен быть принятым.

5. @keysersoze: В целом я не согласен с вашим утверждением. То, что какая-то реализация сохраняла размер до выделения памяти, не означает, что это правило.

Ответ №4:

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

Комментарии:

1. у char нет конструктора / деструктора.

Ответ №5:

Если вам действительно необходимо это сделать, почему бы не отключить посредника (операторы new and delete ) и напрямую вызвать глобальное operator new и operator delete ? (Конечно, если вы пытаетесь использовать операторы new and delete , вам действительно следует переопределить operator new and 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 , если она зарегистрирована).

Комментарии:

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

Ответ №6:

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

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

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

Комментарии:

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

2. Спасибо за продуманный ответ. Голосую за!

3. @Andrew: Боюсь, стандарт довольно ясен на этот счет: «Значением операнда delete может быть значение указателя null, указатель на объект, не являющийся массивом, созданный предыдущим новым выражением , или указатель на подобъект, представляющий базовый класс такого объекта. Если нет, то поведение не определено.» Итак, если компилятор принимает ваш код без диагностики, это не что иное, как ошибка в компиляторе…

4. @KerrekSB — Re это не что иное, как ошибка в компиляторе — Я не согласен. Стандарт гласит, что поведение не определено. Это означает, что компилятор / реализация может делать что угодно и при этом оставаться совместимым со стандартом. Если в ответе компилятора говорится, что вы не можете удалить указатель void *, это нормально. Если ответом компилятора является удаление жесткого диска, это тоже нормально. OTOH, если в ответ компилятор не генерирует никакой диагностики, а вместо этого генерирует код, который освобождает память, связанную с этим указателем, это тоже нормально. Это простой способ справиться с этой формой UB.

5. Просто добавлю: я не одобряю использование delete void_pointer . Это неопределенное поведение. Программисты никогда не должны вызывать неопределенное поведение, даже если кажется, что ответ выполняет то, что хотел программист.

Ответ №7:

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

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

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

Управляющий не будет вызван.

Ответ №8:

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

Ответ №9:

Если вы хотите использовать void *, почему бы вам не использовать просто malloc / free? создание / удаление — это больше, чем просто управление памятью. В принципе, 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);
}
  

Комментарии:

1. Я не писал код в примере — наткнулся на этот шаблон, используемый в паре мест, любопытным образом смешивающий управление памятью C / C , и мне было интересно, каковы конкретные опасности.

2. Написание C / C — это верный путь к провалу. Тот, кто это написал, должен был написать то или другое.

3. @David Это C , а не C / C . C не имеет шаблонов и не использует new и delete.

Ответ №10:

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

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

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

Ответ №11:

Я использовал void *, (он же неизвестные типы) в своей платформе для while in code reflection и других двусмысленных действий, и до сих пор у меня не было проблем (утечка памяти, нарушения доступа и т.д.) Ни от каких компиляторов. Только предупреждения из-за нестандартности операции.

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

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

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

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

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

Ответ №12:

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

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

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

OpaqueBuffer* ptr = new OpaqueBuffer();

delete ptr;
  

Ответ №13:

Для конкретного случая char .

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

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

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

Комментарии:

1. » sizeof(char) обычно равен единице » sizeof(char) всегда равен единице

2. До недавнего времени (2019) люди не думали, что new на самом деле определено для throw . Это не так. Это зависит от компилятора и переключателя компилятора. Смотрите, например, переключатели MSVC2019 /GX[-] enable C EH (same as /EHsc) . Также во встроенных системах многие предпочитают не платить налоги на производительность за исключения C . Таким образом, предложение, начинающееся с «Но вы теряете std::bad_alloc …», вызывает сомнение.