Широкое использование непереносимых приведения типов `void **`

Я считаю, что следующий код четко определен в соответствии со стандартом 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 ** непереносимо? И если да, то почему это кажется обычной практикой?

Как говорится в ответе, это может сработать, но не гарантировано. Поэтому вы, разработчик, должны решать любые случаи, когда это может не работать.

OldBoy 25.07.2024 18:02

1. Вы использовали приведение типов, поэтому компилятор предполагает, что вы знаете, что делаете. 2. Стандарт C не определяет такое поведение, но на практике оно в основном переносимо.

n. m. could be an AI 25.07.2024 18:20

Это зависит от того, что вы делаете с указателями внутри и снаружи f. void * совместим с любым типом указателя, но при назначении void * вы теряете информацию о типе данных или размере объекта. void ** позволяет f изменить указатель p. Во втором случае это неопределенное поведение, если f присваивает значение, которое не является допустимым указателем на double. В первом случае вы должны использовать его как указатель на один и тот же тип данных внутри и снаружи f. При использовании приведения или void* программист несет ответственность за то, чтобы все было правильно. Компилятор может не предупредить вас, если это неправильно.

Bodo 25.07.2024 18:21

Он непереносим, ​​но COM может его использовать, поскольку требования ABI COM более строгие, чем C. Я подозреваю, что аналогичные замечания применимы и к Linux. Не уверен насчет CPython.

Raymond Chen 25.07.2024 18:37

Чтобы сделать вещи более интересными, POSIX утверждает, что *(void **)(&cosine)=dlsym(handle, "cos") — это правильный способ назначить указатель на функцию из dlsym.

dbush 25.07.2024 22:11

Приведение типов в первом фрагменте не требуется; &p уже есть void **, поэтому актерский состав просто запутывает то, что вы делаете. Я рассматриваю второй сценарий, используя: double *p = …; void *vp = p; f(&vp); p = vp; потому что наиболее вероятной причиной передачи двойного указателя является то, что вызываемый код собирается изменить данный ему указатель. Если p не инициализирован, используйте double *p; void *vp = NULL; f(&vp); p = vp;.

Jonathan Leffler 25.07.2024 22:51
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
6
140
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Разыменование результирующего 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; нарушает ограничения выравнивания? Не понимаю, почему два разных указателя будут выровнены по-разному.

Ted Klein Bergman 25.07.2024 19:15

@Тед Кляйн Бергман Это было бы редкостью, если бы оно существовало. Вот почему я сказал, что это будет безопасно в большинстве сред. Возможно, это система, в которой void * больше double *.

ikegami 25.07.2024 19:17

На самом деле приведение неправильное и непереносимое, но большинство компиляторов даже не могут обнаружить проблему.

Предположим, у нас есть архитектура с пословной адресацией, но 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; может быть переупорядочен во время оптимизации. Это всего лишь один пример.

Eric Postpischil 25.07.2024 19:35

@EricPostpischil: Это правда; Мне не удалось построить живой пример из предоставленного фрагмента. В других случаях (например, если указатель не является локальной переменной), скорее всего, произойдет сбой, как вы описываете.

Joshua 25.07.2024 19:37

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

chqrlie 25.07.2024 22:59

@chqrlie: Нет особого смысла. Что на самом деле произойдет на такой платформе, так это то, что стандарт отступит. Мы видели это раньше.

Joshua 25.07.2024 23:27

@chqrlie «Стандарт POSIX требует, чтобы все типы указателей имели одинаковый размер» — это интересно. У вас есть цитата?

chux - Reinstate Monica 30.07.2024 07:10

@chux: Я не могу вспомнить, где я видел это утверждение. Я нашел это Типы указателей POSIX.1-2008 явно требует реализации для преобразования указателей в void * и обратно без потери информации. Это расширение стандарта ISO C. Это почти означает, что указатели на данные должны быть по крайней мере такого же размера, как указатели на функции, что исключает древнюю компактную модель x86.

chqrlie 30.07.2024 12:03

@chux: обратите также внимание, что все указатели на структуры должны иметь одинаковый размер, чтобы обеспечить компиляцию кода, обрабатывающего неполные типы. То же ограничение применяется к указателям на типы union.

chqrlie 30.07.2024 12:20

Другие вопросы по теме