Я читал различные сообщения о переполнении стека RE: ошибка разыменовывающего указателя типа. Насколько я понимаю, ошибка, по сути, является предупреждением компилятора об опасности доступа к объекту через указатель другого типа (хотя, похоже, для этого сделано исключение char*
), что является понятным и разумным предупреждением.
Мой вопрос относится к приведенному ниже коду: почему приведение адреса указателя к void**
значению квалифицируется для этого предупреждения (повышается до ошибки через -Werror
)?
Более того, этот код скомпилирован для нескольких целевых архитектур, только одна из которых генерирует предупреждение / ошибку - может ли это означать, что это законно является недостатком конкретной версии компилятора?
// main.c
#include <stdlib.h>
typedef struct Foo
{
int i;
} Foo;
void freeFunc( void** obj )
{
if ( obj && * obj )
{
free( *obj );
*obj = NULL;
}
}
int main( int argc, char* argv[] )
{
Foo* f = calloc( 1, sizeof( Foo ) );
freeFunc( (void**)(&f) );
return 0;
}
Если мое понимание, изложенное выше, является правильным, то void**
, будучи все еще просто указателем, это должно быть безопасное приведение.
Есть ли обходной путь, не использующий lvalues, который бы успокоил это специфичное для компилятора предупреждение / ошибку? Т.е. я понимаю, что и почему это решит проблему, но я хотел бы избежать этого подхода, потому что я хочу использовать freeFunc()
NULL для получения предполагаемого out-arg:
void* tmp = f;
freeFunc( &tmp );
f = NULL;
Компилятор проблемы (один из одного):
user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules
user@8d63f499ed92:/build$
Компилятор без жалоб (один из многих):
user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
user@8d63f499ed92:/build$
Обновление: я также обнаружил, что предупреждение, похоже, генерируется специально при компиляции -O2
(все еще с отмеченным только "проблемным компилятором")
void**
, оставаясь просто указателем, это должно быть безопасное приведение». Вау там скиппи! Похоже, у вас есть некоторые фундаментальные предположения. Постарайтесь меньше думать с точки зрения байтов и рычагов и больше с точки зрения абстракций, потому что это то, с чем вы на самом деле программируетеОтветы:
Значение типа
void**
- это указатель на объект типаvoid*
. Объект типаFoo*
не является объектом типаvoid*
.Существует неявное преобразование между значениями типа
Foo*
иvoid*
. Это преобразование может изменить представление значения. Точно так же вы можете писать,int n = 3; double x = n;
и это имеет четко определенное поведение установкиx
значения3.0
, ноdouble *p = (double*)&n;
имеет неопределенное поведение (и на практике не будет устанавливатьp
«указатель на3.0
» в любой общей архитектуре).Архитектуры, в которых разные типы указателей на объекты имеют разные представления, сегодня редки, но они разрешены стандартом C. Существуют (редкие) старые машины с указателями слов, которые являются адресами слова в памяти, и указателями байтов, которые являются адресами слова вместе со смещением байтов в этом слове;
Foo*
будет указателем слова иvoid*
будет указателем байта на таких архитектурах. Существуют (редкие) машины с толстыми указателями, которые содержат информацию не только об адресе объекта, но также о его типе, размере и списках контроля доступа; указатель на определенный тип может иметь представление, отличное от того,void*
которое требует дополнительной информации о типе во время выполнения.Такие машины редки, но разрешены стандартом C. И некоторые компиляторы C пользуются преимуществом разрешения обрабатывать указатели типа как отдельные для оптимизации кода. Риск создания псевдонимов указателей является основным ограничением способности компилятора оптимизировать код, поэтому компиляторы, как правило, используют такие разрешения.
Компилятор может сказать вам, что вы делаете что-то не так, или тихо делать то, что вы не хотели, или тихо делать то, что вы хотели. Неопределенное поведение допускает любое из них.
Вы можете сделать
freefunc
макрос:Это сопровождается обычными ограничениями макросов: отсутствие безопасности типов
p
оценивается дважды. Обратите внимание, что это только дает вам возможность не оставлять висячие указатели, еслиp
был единственный указатель на освобожденный объект.источник
Foo*
иvoid*
имеют такое же представление о вашей архитектуры, это еще не определено для типа-каламбур них.Стандарт A
void *
специально обрабатывается отчасти потому, что он ссылается на неполный тип. Эта процедура никак не распространяется на ,void **
как это делает точку полного типа, в частностиvoid *
.Строгие правила псевдонимов говорят, что вы не можете конвертировать указатель одного типа в указатель другого типа и впоследствии разыменовывать этот указатель, потому что это означает, что байты одного типа следует интерпретировать как другой. Единственное исключение - при преобразовании в символьный тип, который позволяет читать представление объекта.
Вы можете обойти это ограничение, используя вместо функции функциональный макрос:
Который вы можете назвать так:
Это, однако, имеет ограничение, потому что вышеупомянутый макрос будет оцениваться
obj
дважды. Если вы используете GCC, этого можно избежать с помощью некоторых расширений, в частностиtypeof
ключевых слов и выражений операторов:источник
#define
том, что она будет оцениватьсяobj
дважды. Хотя я не знаю, как избежать второй оценки. Даже выражение оператора (расширение GNU) не справится с задачей, которую вам нужно назначитьobj
после того, как вы использовали его значение.typeof
чтобы избежать оценокobj
дважды#define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
.Разыменование перенаправленного указателя типа - UB, и вы не можете рассчитывать на то, что произойдет.
Разные компиляторы генерируют разные предупреждения, и для этой цели разные версии одного и того же компилятора могут рассматриваться как разные компиляторы. Это, кажется, лучшее объяснение дисперсии, которую вы видите, чем зависимость от архитектуры.
Случай, который может помочь вам понять, почему в этом случае может быть плохое типизирование, заключается в том, что ваша функция не будет работать на архитектуре, для которой
sizeof(Foo*) != sizeof(void*)
. Это разрешено стандартом, хотя я не знаю ни одного текущего, для которого это правда.Обходной путь должен был бы использовать макрос вместо функции.
Обратите внимание, что
free
принимает нулевые указатели.источник
sizeof Foo* != sizeof void*
. Я никогда не сталкивался с «дикими» размерами указателей, зависящими от типа, поэтому с годами я пришел к выводу, что аксиоматично, что размеры указателей одинаковы для данной архитектуры.float*
не изменяетint32_t
объект, так что, например, компиляторуint32_t*
не нужноint32_t *restrict ptr
предполагать, что он не указывает на одну и ту же память. То же самое для магазинов черезvoid**
существо, предполагаемое не изменятьFoo*
объект.Этот код недопустим в соответствии со стандартом C, поэтому он может работать в некоторых случаях, но не обязательно переносимый.
«Правило строгого псевдонима» для доступа к значению через указатель, который был приведен к другому типу указателя, находится в 6.5, параграф 7:
В вашем
*obj = NULL;
утверждении объект имеет эффективный тип,Foo*
но к нему обращается выражение lvalue*obj
с типомvoid*
.В пункте 2 пункта 6.7.5.1 мы имеем
Так что
void*
иFoo*
не являются совместимыми типами или совместимыми типами с добавленными квалификаторами, и, конечно, не соответствуют ни одному из других параметров правила строгого алиасинга.Хотя это и не техническая причина, по которой код является недействительным, это также относится к примечанию раздела 6.2.5, пункт 26:
Что касается различий в предупреждениях, то это не тот случай, когда стандарт требует диагностического сообщения, поэтому вопрос только в том, насколько хорош компилятор или его версия в том, что он замечает потенциальные проблемы и указывает на них полезным способом. Вы заметили, что настройки оптимизации могут иметь значение. Часто это происходит из-за того, что внутренне генерируется больше информации о том, как различные части программы на самом деле сочетаются друг с другом на практике, и поэтому дополнительная информация также доступна для проверок предупреждений.
источник
В дополнение к тому, что сказали другие ответы, это классический анти-паттерн в C, и тот, который должен быть сожжен огнем. Появляется в:
void *
(который не страдает от этой проблемы, потому что он включает преобразование значения вместо типа punning ), вместо этого возвращая флаг ошибки и сохраняя результат через указатель на указатель.Для другого примера (1) в функции ffmpeg / libavcodec был давний печально известный случай
av_free
. Я полагаю, что в конечном итоге это было исправлено с помощью макроса или другого трюка, но я не уверен.Для (2) оба
cudaMalloc
иposix_memalign
являются примерами.Ни в том, ни в другом случае интерфейс по своей природе не требует недопустимого использования, но он настоятельно рекомендует его и допускает правильное использование только с дополнительным временным объектом типа,
void *
который побеждает назначение функциональности free-and-null-out и делает распределение неудобным.источник
void *
и преобразовывать его каждый раз, когда он хочет разыменовать его. Это очень маловероятно. Если вызывающая сторона хранит какой-то другой тип указателя, единственный способ вызвать функцию без вызова UB - это скопировать указатель на временный объект типаvoid *
и передать его адрес функции освобождения, а затем просто ...(void **)
приведение, что приводит к неопределенному поведению.Хотя C был разработан для машин, которые используют одно и то же представление для всех указателей, авторы Стандарта хотели сделать язык пригодным для использования на машинах, которые используют разные представления для указателей на разные типы объектов. Следовательно, они не требовали, чтобы машины, которые используют разные представления указателей для разных типов указателей, поддерживали тип «указатель на любой вид указателя», хотя многие машины могли сделать это с нулевой стоимостью.
До написания Стандарта реализации для платформ, которые использовали одно и то же представление для всех типов указателей, единодушно позволяли
void**
бы использовать, по крайней мере, с подходящим приведением, в качестве «указателя на любой указатель». Авторы Стандарта почти наверняка признали, что это будет полезно на платформах, которые его поддерживают, но, поскольку он не может быть поддержан повсеместно, они отказались от его мандата. Вместо этого они ожидали, что качественная реализация обработает такие конструкции, которые Rationale назвал бы «популярным расширением», в тех случаях, когда это имеет смысл.источник