Я считаю, что следующий код четко определен в соответствии со стандартом C:
void f(void **);
void *p;
f((void **)&p);
Но что, если p
не является указателем на void
, скажем:
void f(void **);
double *p;
f((void **)&p);
Согласно FAQ по C , он не переносимый. Однако это не приводит к тому, что мой компилятор выдает какие-либо предупреждения, а поиск в хорошо известных базах кода C дает множество примеров таких не-void *
-приведений, например, Linux, CPython и COM все это делают. В ответах на наиболее близкий вопрос SO, который я смог найти, не упоминается, что это странно или нестандартно. Итак, действительно ли такое использование void **
непереносимо? И если да, то почему это кажется обычной практикой?
1. Вы использовали приведение типов, поэтому компилятор предполагает, что вы знаете, что делаете. 2. Стандарт C не определяет такое поведение, но на практике оно в основном переносимо.
Это зависит от того, что вы делаете с указателями внутри и снаружи f
. void *
совместим с любым типом указателя, но при назначении void *
вы теряете информацию о типе данных или размере объекта. void **
позволяет f
изменить указатель p
. Во втором случае это неопределенное поведение, если f
присваивает значение, которое не является допустимым указателем на double
. В первом случае вы должны использовать его как указатель на один и тот же тип данных внутри и снаружи f
. При использовании приведения или void*
программист несет ответственность за то, чтобы все было правильно. Компилятор может не предупредить вас, если это неправильно.
Он непереносим, но COM может его использовать, поскольку требования ABI COM более строгие, чем C. Я подозреваю, что аналогичные замечания применимы и к Linux. Не уверен насчет CPython.
Чтобы сделать вещи более интересными, POSIX утверждает, что *(void **)(&cosine)=dlsym(handle, "cos")
— это правильный способ назначить указатель на функцию из dlsym
.
Приведение типов в первом фрагменте не требуется; &p
уже есть void **
, поэтому актерский состав просто запутывает то, что вы делаете. Я рассматриваю второй сценарий, используя: double *p = …; void *vp = p; f(&vp); p = vp;
потому что наиболее вероятной причиной передачи двойного указателя является то, что вызываемый код собирается изменить данный ему указатель. Если p
не инициализирован, используйте double *p; void *vp = NULL; f(&vp); p = vp;
.
Разыменование результирующего void **
является неопределенным поведением, поэтому вы также можете использовать void *
.
double *p;
void **vp = (void **)p; // UB if it violates alignment restrictions.
*vp = NULL; // UB
Если void *
и double *
имеют разные ограничения выравнивания, поведение не определено. В противном случае допускается.
C17 §6.3.2.3 ¶7 Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель неправильно выровнен для ссылочного типа, поведение не определено. В противном случае при повторном преобразовании результат будет равен исходному указателю. Когда указатель на объект преобразуется в указатель на символьный тип, результат указывает на младший адресуемый байт объекта. Последовательные приращения результата до размера объекта дают указатели на оставшиеся байты объекта.
Таким образом, сам гипс должен быть безопасным в большинстве сред. Но что, если вы разыменуете void **
?
Это «строгое нарушение псевдонимов» и, следовательно, неопределенное поведение.
C17 §6.5 ¶7 Сохраненное значение объекта должно быть доступно только с помощью выражения lvalue, которое имеет один из следующие типы:
- тип, совместимый с эффективным типом объекта,
- квалифицированная версия типа, совместимая с эффективным типом объекта,
- тип, который является знаковым или беззнаковым типом, соответствующим эффективному типу объекта,
- тип, который является знаковым или беззнаковым типом, соответствующим квалифицированной версии действующего тип объекта,
- тип агрегата или объединения, который включает в себя один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член подагрегата или содержащегося объединения), или
- тип персонажа.
Есть ли случай, когда void **vp = (void **)p;
нарушает ограничения выравнивания? Не понимаю, почему два разных указателя будут выровнены по-разному.
@Тед Кляйн Бергман Это было бы редкостью, если бы оно существовало. Вот почему я сказал, что это будет безопасно в большинстве сред. Возможно, это система, в которой void *
больше double *
.
На самом деле приведение неправильное и непереносимое, но большинство компиляторов даже не могут обнаружить проблему.
Предположим, у нас есть архитектура с пословной адресацией, но double
имеет длину 80 бит и использует частичные слова. (Это гораздо более вероятно с char
или long double
, чем с double
, я отвлекся.)
Мы можем получить sizeof(int *) == 4
, но sizeof(double *) == 8
; следовательно, sizeof(void *)
тоже должно быть 8; однако sizeof (void **)
легко может быть 4.
Это приводит к тому, что функция f
отсекает часть указателя, и код не работает. Компилятор для платформы, которая действительно демонстрирует такое поведение, успешно предупредит об этом; однако большинство компиляторов ориентированы только на архитектуры, для которых все размеры указателей одинаковы, и не имеют способа обнаружить проблему.
Платформы, которые ведут себя подобным образом, по большей части исчезли, и никого за пределами встроенного мира это не волнует. Единственный, с которым я когда-либо сталкивался, был противным и совершенно непереносимым, потому что const void *
и void *
были разных размеров из-за того, что ПЗУ было больше, чем размер слова ЦП, а ОЗУ - нет.
Хотя этот конкретный образец имеет тенденцию работать; большинство примеров этой формы также нестабильны из-за нарушения строгого правила псевдонимов.
Обратите внимание, что соображения, связанные с аппаратным обеспечением, — не единственная причина, по которой программа на языке C может выйти из строя при неопределенном поведении. Даже если double *
и void *
имеют одинаковый размер и представление, компилятор может в различных ситуациях принять за предпосылку, что d
типа double **
и v
типа void **
не указывают на одну и ту же память из-за правил псевдонимов. , и поэтому *d = foo; bar = *v;
может быть переупорядочен во время оптимизации. Это всего лишь один пример.
@EricPostpischil: Это правда; Мне не удалось построить живой пример из предоставленного фрагмента. В других случаях (например, если указатель не является локальной переменной), скорее всего, произойдет сбой, как вы описываете.
Вы могли бы упомянуть, что стандарт POSIX требует, чтобы все типы указателей имели одинаковый размер и представление, поэтому приведение не может иметь неблагоприятный побочный эффект, который вы иллюстрируете. Тем не менее, строгое нарушение псевдонимов может вызвать очень неприятные ошибки в коде, использующем такие приведения: это объясняет, почему компиляторы стали гораздо более красноречивыми при повышенных уровнях предупреждений о невинно выглядящем, но потенциально вредном коде, даже в POSIX-совместимых системах.
@chqrlie: Нет особого смысла. Что на самом деле произойдет на такой платформе, так это то, что стандарт отступит. Мы видели это раньше.
@chqrlie «Стандарт POSIX требует, чтобы все типы указателей имели одинаковый размер» — это интересно. У вас есть цитата?
@chux: Я не могу вспомнить, где я видел это утверждение. Я нашел это Типы указателей POSIX.1-2008 явно требует реализации для преобразования указателей в void *
и обратно без потери информации. Это расширение стандарта ISO C. Это почти означает, что указатели на данные должны быть по крайней мере такого же размера, как указатели на функции, что исключает древнюю компактную модель x86.
@chux: обратите также внимание, что все указатели на структуры должны иметь одинаковый размер, чтобы обеспечить компиляцию кода, обрабатывающего неполные типы. То же ограничение применяется к указателям на типы union
.
Как говорится в ответе, это может сработать, но не гарантировано. Поэтому вы, разработчик, должны решать любые случаи, когда это может не работать.