Рассмотрим следующий код 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
}
Содержит ли этот код какое-либо неопределенное поведение?
Мое собственное понимание:
S, поэтому оно не нарушает правило строгого псевдонимов (C17 §6.5.7)Opaque не определяется как имеющий первый член типа int, поэтому кажется, что 6.7.2.15 не применяется (неясное значение слова «соответственно преобразованное» имеет тенденцию мутить воду здесь). И строгое правило псевдонимов, похоже, не позволяет получить доступ к объекту эффективного типа S через указатель int (хотя и допускает обратное). Итак, является ли утверждение 3 неопределенным поведением или нет?@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* будут участвовать в анализе псевдонимов?
Re: «соответствующим образом преобразовано»: Мне все время интересно, что именно это означает. Другими словами: каково точное определение/правила понятия «соответственно преобразованный». @EricPostpischil Разве стандарт C не определяет «соответствующее преобразование» намеренно или это дефект (возможно, есть DR)?





Стандарт был написан в эпоху, когда авторы компиляторов не начали использовать языковых юристов для оправдания бесполезного поведения, и поэтому он не может определить свою терминологию однозначно, чтобы противостоять языковому праву. Более того, когда был написан 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 года.
@NateEldredge: Изменилось ли что-нибудь, имеющее отношение к вопросу, между C99 и C17? Ничто в C89 не предполагало, что поведение structPtr->member не должно быть независимым от всего, кроме существования, типа и смещения именованного члена внутри типа structPtr, а также что (struct s2*)(struct s1*)structPtr и (void*)(struct s1*)structPtr не должны быть независимыми от определения struct s1; хотя C89 прямо не оговаривал такую эквивалентность во всех случаях, когда она должна применяться, эквивалентность применялась глобально на языке, для описания которого был создан Стандарт, и...
... ничто в C89 или опубликованном Обосновании не предполагало какого-либо намерения изменить это. C99 намеренно дал реализациям разрешение отклоняться от этих эквивалентностей, не предпринимая никаких систематических усилий по выявлению всех мест, где они должны соблюдаться. Хотя точный код ОП не создавал никаких проблем с выравниванием, такие проблемы могут легко возникнуть в аналогичном коде, особенно если кто-то приводит указатели на типы объединения, пытаясь обойти нарушающие код интерпретации правил псевдонимов на основе типов. Разве вы не считаете вопросы согласования актуальными?
Утверждение 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 не определяет «соответствующее преобразование». Правила псевдонимов касаются только типа, используемого для доступа; преобразования промежуточных указателей не имеют значения. И по нужному адресу стоит
int. Таким образом, единственный вопрос заключается в том, сохранится ли желаемое значение указателя после преобразований.