c - Почему эта заявка на разыменование предупреждающего указателя типа зависит от компилятора?




pointers casting (4)

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

  1. Функции освобождения и обнуления, подобные той, в которой вы нашли предупреждение.
  2. Функции распределения, которые избегают стандартного языка C возврата void * (который не страдает от этой проблемы, потому что он включает преобразование значения вместо типа punning ), вместо этого возвращая флаг ошибки и сохраняя результат через указатель на указатель.

Для другого примера (1) в функции ffmpeg / libavcodec av_free был давний печально известный случай. Я полагаю, что в конечном итоге это было исправлено с помощью макроса или другого трюка, но я не уверен.

Для (2) оба cudaMalloc и posix_memalign .

Ни в том, ни в другом случае интерфейс по своей природе не требует недопустимого использования, но он настоятельно рекомендует его и допускает правильное использование только с дополнительным временным объектом типа void * который отрицательно сказывается на функциональности free-and-null-out и делает распределение неудобным ,

Я читал various posts on переполнении стека 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() аргумент:

void* tmp = f;
freeFunc( &tmp );
f = NULL;

Компилятор проблемы (один из одного):

[email protected]:/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

[email protected]:/build$

Компилятор без жалоб (один из многих):

[email protected]:/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.

[email protected]:/build$

Обновление: я также обнаружил, что предупреждение создается специально при компиляции с -O2 (все еще только с отмеченным «проблемным компилятором»)


Значение типа void** является указателем на объект типа void* . Объект типа Foo* не является объектом типа void* .

Существует неявное преобразование между значениями типа Foo* и void* . Это преобразование может изменить представление значения. Точно так же вы можете написать int n = 3; double x = n; int n = 3; double x = n; и это имеет четко определенное поведение установки x на значение 3.0 , но double *p = (double*)&n; имеет неопределенное поведение (и на практике не будет устанавливать p в «указатель на 3.0 » в любой обычной архитектуре).

Архитектуры, в которых разные типы указателей на объекты имеют разные представления, сегодня редки, но они разрешены стандартом C. Существуют (редкие) старые машины с указателями слов, которые являются адресами слова в памяти, и указателями байтов, которые являются адресами слова вместе со смещением байтов в этом слове; Foo* будет указателем слова, а void* будет указателем байта на таких архитектурах. Существуют (редкие) машины с толстыми указателями, которые содержат информацию не только об адресе объекта, но также о его типе, размере и списках контроля доступа; указатель на определенный тип может иметь представление, отличное от void* которому требуется дополнительная информация о типе во время выполнения.

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

Компилятор может сказать вам, что вы делаете что-то не так, или тихо делать то, что вы не хотели, или тихо делать то, что вы хотели. Неопределенное поведение допускает любое из них.

Вы можете сделать freefunc макрос:

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

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


Хотя C был разработан для машин, которые используют одно и то же представление для всех указателей, авторы Стандарта хотели сделать язык пригодным для использования на машинах, которые используют разные представления для указателей на разные типы объектов. Следовательно, они не требовали, чтобы машины, которые используют разные представления указателей для разных типов указателей, поддерживали тип «указатель на любой вид указателя», хотя многие машины могли сделать это с нулевой стоимостью.

До написания Стандарта реализации для платформ, которые использовали одно и то же представление для всех типов указателей, единодушно позволяли использовать void** , по крайней мере, с подходящим приведением типа, в качестве «указателя на любой указатель». Авторы Стандарта почти наверняка признали, что это будет полезно на платформах, которые его поддерживают, но, поскольку он не может быть поддержан повсеместно, они отказались от его мандата. Вместо этого они ожидали, что качественная реализация обработает такие конструкции, которые Rationale назвал бы «популярным расширением», в тех случаях, когда это имеет смысл.


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

«Правило строгого псевдонима» для доступа к значению через указатель, который был приведен к другому типу указателя, находится в 6.5, параграф 7:

Объект должен иметь свое сохраненное значение, доступное только через выражение lvalue, которое имеет один из следующих типов:

  • тип, совместимый с эффективным типом объекта,

  • квалифицированная версия типа, совместимого с эффективным типом объекта,

  • тип, который является типом со знаком или без знака, соответствующим действующему типу объекта,

  • тип, который является типом со знаком или без знака, соответствующим квалифицированной версии действующего типа объекта,

  • агрегатный или объединенный тип, который включает в себя один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член субагрегированного или автономного объединения), или

  • тип персонажа.

В вашем *obj = NULL; утверждение, объект имеет эффективный тип Foo* но доступ к нему осуществляется через выражение lvalue *obj с типом void* .

В пункте 2 пункта 6.7.5.1 мы имеем

Для совместимости двух типов указателей оба должны быть одинаково квалифицированы, и оба должны быть указателями на совместимые типы.

Таким образом, void* и Foo* не являются совместимыми типами или совместимыми типами с добавленными квалификаторами и, конечно, не подходят ни к каким другим параметрам правила строгого алиасинга.

Хотя это и не техническая причина, по которой код является недействительным, это также относится к примечанию раздела 6.2.5, пункт 26:

Указатель на void должен иметь те же требования к представлению и выравниванию, что и указатель на символьный тип. Аналогично, указатели на квалифицированные или неквалифицированные версии совместимых типов должны иметь одинаковые требования к представлению и выравниванию. Все указатели на типы конструкций должны иметь те же требования к представлению и выравниванию, что и другие. Все указатели на типы объединения должны иметь те же требования к представлению и выравниванию, что и другие. Указатели на другие типы не обязательно должны иметь одинаковые требования к представлению или выравниванию.

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







casting