Строгое псевдонимирование первого члена структуры через непрозрачный указатель в C

Рассмотрим следующий код ISO C:

struct S {
    int a;
};

void f() {
    struct S s;
    *(int*)&s = 1;  // 1
    ((struct S*)(struct Opaque*)&s)->a = 2;  // 2
    *(int*)(struct Opaque*)&s = 3;  // 3
}

Содержит ли этот код какое-либо неопределенное поведение?

Мое собственное понимание:

  • Утверждение 1 не вызывает неопределенное поведение из-за C17 §6.7.2.15 «Указатель на объект структуры, преобразованный соответствующим образом, указывает на его начальный член и наоборот»
  • Утверждение 2 не вызывает неопределенного поведения, несмотря на приведение типов, поскольку мы обращаемся к объекту через его эффективный тип S, поэтому оно не нарушает правило строгого псевдонимов (C17 §6.5.7)
  • А как насчет утверждения 3? Opaque не определяется как имеющий первый член типа int, поэтому кажется, что 6.7.2.15 не применяется (неясное значение слова «соответственно преобразованное» имеет тенденцию мутить воду здесь). И строгое правило псевдонимов, похоже, не позволяет получить доступ к объекту эффективного типа S через указатель int (хотя и допускает обратное). Итак, является ли утверждение 3 неопределенным поведением или нет?

Это не может быть должным образом согласовано с языковыми законами, поскольку стандарт C не определяет «соответствующее преобразование». Правила псевдонимов касаются только типа, используемого для доступа; преобразования промежуточных указателей не имеют значения. И по нужному адресу стоит int. Таким образом, единственный вопрос заключается в том, сохранится ли желаемое значение указателя после преобразований.

Eric Postpischil 02.06.2024 01:20

@EricPostpischil: Учитывая void *p=something(); struct s1 { int arr[i]; } *p1=p; struct s2 { int arr[3]; }; *p2=p, можно ли использовать выражения p1->arr[0] и p2->arr[0] для доступа к произвольным объектам типа int, учитывая, что (p1->arr+0) и (p2->arr+0) оба являются выражениями типа int*, или промежуточные типы указателей struct s1* и struct s2* будут участвовать в анализе псевдонимов?

supercat 02.06.2024 18:40

Re: «соответствующим образом преобразовано»: Мне все время интересно, что именно это означает. Другими словами: каково точное определение/правила понятия «соответственно преобразованный». @EricPostpischil Разве стандарт C не определяет «соответствующее преобразование» намеренно или это дефект (возможно, есть DR)?

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

Ответы 2

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

С другой стороны, на некоторых популярных платформах возникают ситуации, когда на поведение кода, преобразующего указатель через указатель в структуру или тип объединения, может влиять требование выравнивания последнего типа. При обработке clang это может произойти даже с объединениями, к которым никогда не обращались с использованием типов с более грубым выравниванием. Рассмотрим, например:

#include <string.h>
struct s1 { unsigned arr; };
struct s2 { unsigned char s[sizeof (struct s1)]; };
void test(void *dest, void *src)
{
  memcpy((struct s1*)dest, (struct s1*)src, sizeof (struct s1));
}

Код clang, генерируемый для ARM Cortex-M0, завершится ошибкой, если p не выровнен по 32-битному формату, даже при использовании -fno-strict-aliasing. Такие потенциальные различия в поведении для разных типов структур, казалось бы, не должны иметь значения в тех случаях, когда для типа нет никакого определения, но при использовании «Всй оптимизации программы» компилятор может интерпретировать отсутствие какого-либо определения структуры, которое потребует от него работы с указателями произвольного выравнивания, что оправдывает предположение, что указатели, преобразованные в этот тип, будут удовлетворять даже самым грубым требованиям выравнивания.

Стандарт не вникает в последствия применения множественных преобразований к указателю, поскольку авторы не хотели ни подразумевать, что все компиляторы должны придавать семантическое значение промежуточным преобразованиям, ни подразумевать, что компилятор, которому задано выражение типа *(unsigned*)floatPtr, должен игнорировать тип floatPtr при принятии решения о том, может ли это выражение получить доступ к хранилищу, которое используется объектом типа float. К сожалению, некоторые авторы компиляторов меньше интересуются тем, что им нужно сделать для полезной обработки программ, чем поиском оправданий не делать этого, и интерпретируют отсутствие конкретики в Стандарте как приглашение к бессмысленному поведению.

Мы говорим о C17, и я думаю, что «лингвистический юрист», который вы осуждаете, был обычным явлением задолго до 2017 года.

Nate Eldredge 03.06.2024 17:50

@NateEldredge: Изменилось ли что-нибудь, имеющее отношение к вопросу, между C99 и C17? Ничто в C89 не предполагало, что поведение structPtr->member не должно быть независимым от всего, кроме существования, типа и смещения именованного члена внутри типа structPtr, а также что (struct s2*)(struct s1*)structPtr и (void*)(struct s1*)structPtr не должны быть независимыми от определения struct s1; хотя C89 прямо не оговаривал такую ​​эквивалентность во всех случаях, когда она должна применяться, эквивалентность применялась глобально на языке, для описания которого был создан Стандарт, и...

supercat 03.06.2024 18:27

... ничто в C89 или опубликованном Обосновании не предполагало какого-либо намерения изменить это. C99 намеренно дал реализациям разрешение отклоняться от этих эквивалентностей, не предпринимая никаких систематических усилий по выявлению всех мест, где они должны соблюдаться. Хотя точный код ОП не создавал никаких проблем с выравниванием, такие проблемы могут легко возникнуть в аналогичном коде, особенно если кто-то приводит указатели на типы объединения, пытаясь обойти нарушающие код интерпретации правил псевдонимов на основе типов. Разве вы не считаете вопросы согласования актуальными?

supercat 03.06.2024 18:34
Ответ принят как подходящий

Утверждение 1 не вызывает неопределенное поведение из-за C17 §6.7.2.15 «Указатель на объект структуры, преобразованный соответствующим образом, указывает на его начальный член и наоборот»

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


Утверждение 2 не вызывает неопределенного поведения, несмотря на приведения, поскольку мы обращаемся к объекту через его эффективный тип S.

Не обязательно. C17 6.3.2.3 говорит:

Указатель на тип объекта может быть преобразован в указатель на другой тип объекта. Если результирующий указатель неправильно выровнен для ссылочного типа, поведение не определено.

Это может привести к сбою во время самого преобразования указателя (struct Opaque*)&s. На практике это маловероятно, но теоретически возможно.

В остальном (struct S*) ... ->a соответствует доступу lvalue с тем же эффективным типом struct S, с которым был объявлен объект, так что эта часть четко определена.


А как насчет утверждения 3?

То же самое и здесь: преобразования указателей не обязательно четко определены.

Кроме того, неразумно говорить, что к агрегату нельзя получить доступ через тип элемента/члена этого агрегата. Одним из больших недостатков строгих правил псевдонимов является то, что они действительно не упоминают, какой эффективный тип имеет агрегат (или объединение), а также что происходит с квалификаторами с точки зрения эффективного типа. int arr[5]; один объект эффективного типа int [5] или 5 объектов эффективного типа int? Это не указано явно, поэтому никто не знает. Это становится вопросом «качества реализации».

Рассмотрим struct S* s = malloc(sizeof *s); s->a = 1;. Или int* a = malloc(int[n]); a[0] = 1;. malloc возвращает указатель на объект без эффективного типа. Он не получит его до первого доступа на запись lvalue. В обоих моих примерах мы получаем доступ к элементу/элементу массива типа int. Если это означает, что тип этой ячейки памяти теперь является скаляром int, а не совокупной структурой/массивом, весь язык C развалится. Потому что тогда, например, a[1] = 1; внезапно станет доступом за пределы скаляра. Это было бы смешно.

Единственная помощь, которую мы можем получить от языка C, это версия 7.22.3, в которой говорится, что возвращаемая память должна использоваться как массив (но структуры/объединения не упоминаются). Однако это правило не согласуется с концепцией эффективного типа.

Итак, ответ: никто не знает. Языковой стандарт здесь бесполезен и неясен.

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

Определение макросов с тем же именем, что и функции стандартной библиотеки
Является ли передача ссылки на примитив из контекста C++ в контекст C неопределенным поведением?
Использование оператора безопасного вызова Kotlin
Является ли неопределенным поведение передача указателя на несконструированный объектstreambuf в конструктор ostream?
Является ли рекурсивный вызов main из его собственных параметров (злоупотребление sizeof с помощью VLA) стандартом C99?
Перегрузка и нестабильность
До C++11 «правило одного определения» нарушалось при инициализации членов класса нестатических и неконстантных переменных. Почему?
Что произойдет, если я вызову allocate_at_least(0) согласно стандарту C++23?
Почему изменяемая лямбда преобразуется в указатель на функцию вместо вызова оператора()?
Могут ли `::` и `*`, образующие тип указателя-члена, происходить из разных расширений макроса или они должны появляться как один токен?