Можно ли использовать указатель на член структуры для доступа к другому члену той же структуры?

Я пытаюсь понять, как работает каламбур, когда дело доходит до сохранения значения в члене структуры или объединения.

Стандарт N1570 6.2.6.1(p6) определяет, что

Когда значение хранится в объекте типа структуры или объединения, в том числе в объекте-члене, байты представления объекта, соответствующие любым байтам заполнения, принимают неопределенные значения.

Поэтому я интерпретировал это так, как будто у нас есть объект для хранения в члене, так что размер объекта равен sizeof(declared_type_of_the_member) + padding, байты, связанные с заполнением, будут иметь неопределенное значение (даже несмотря на то, что у нас были байты в исходном объекте определенный). Вот пример:

struct first_member_padded_t{
    int a;
    long b;
};

int a = 10;
struct first_member_padded_t s;
char repr[offsetof(struct first_member_padded_t, b)] = //some value
memcpy(repr, &a, sizeof(a));
memcpy(&(s.a), repr, sizeof(repr));
s.b = 100;
printf("%d%ld\n", s.a, s.b); //prints 10100

На моей машине sizeof(int) = 4, offsetof(struct first_member_padded_t, b) = 8.

Хорошо ли определено поведение печати 10100 для такой программы? Я думаю, что это так.


person Some Name    schedule 11.03.2019    source источник
comment
Я не вижу здесь ничего, что не было бы четко определено. Вы обеспокоены тем, что значения заполнения будут перезаписаны некоторыми другими неопределенными значениями?   -  person Eugene Sh.    schedule 11.03.2019
comment
вы проверяете, что заполнение находится после поля a (между a и b тогда), в случае, если заполнение (как ни странно) размещено перед < i> a ваш код не устанавливает s.a в 10. Норма явно указывает, где добавляется отступ (извините, слишком ленив, чтобы проверить ^^)?   -  person bruno    schedule 11.03.2019
comment
@EugeneSh. Вы обеспокоены тем, что значения заполнения будут перезаписаны некоторыми другими неопределенными значениями? Совершенно верно. Указывается, что байты имеют неопределенное значение, но есть байты, соответствующие заполнению в repr[offsetof(struct first_member_padded_t, b)]   -  person Some Name    schedule 11.03.2019
comment
Код мне кажется подозрительным. Если между s.a и s.b есть отступы, вы изменяете память за пределами s.a с помощью указателя, производного от s.a. Учитывая, насколько хитрые правила в отношении них, этот код вызывает проблемы.   -  person user694733    schedule 11.03.2019
comment
@ user694733 you are modifying memory outside of s.a Я сделал это намеренно, чтобы проверить процитированное мной правило. Я ожидал, что байты заполнения будут игнорироваться или принимать неопределенное значение.   -  person Some Name    schedule 11.03.2019
comment
Что ж, я понимаю, о чем говорит @ user694733, это не о процитированном правиле, а о записи за пределы объекта s.a, который не определен.   -  person Eugene Sh.    schedule 11.03.2019
comment
@SomeName ваш код позволяет проверить, имеют ли байты заполнения неопределенное значение. Он проверяет заполнение после поля a (иначе s.a не равно 10). Ваш первый memcpy установил 4 первых байта, выделенных для поля a, следующие 4 байта заполнения - undet, вторая копия memspy из смещение поля a 4 байта со значением 10, затем следующие 4 байта с неопределенным значением, затем доступ printf к полю a, вы никогда не читайте отступы в s   -  person bruno    schedule 11.03.2019
comment
@EugeneSh. Я согласен с тем, что запись вне пределов не определена. Итак, правило заключается в том, является ли член структурой или самим объединением. И если мы сохраним значение в байтах-членах, соответствующие любому заполнению, будут принимать неопределенное значение. Это правильно?   -  person Some Name    schedule 11.03.2019
comment
@bruno В любом случае это, похоже, не определено, как упоминал user694733. Я сохранил значение за пределами объекта ...   -  person Some Name    schedule 11.03.2019
comment
вы храните вне поля sa, но не вне поля s, если заполнение находится после поля a, весь вопрос в том, где заполнение является   -  person bruno    schedule 11.03.2019
comment
@bruno Не может быть отступа перед первым членом struct. Указатель на struct может быть преобразован в указатель на его первый член.   -  person alx    schedule 11.03.2019
comment
@bruno Но lvalue, которое я использовал для хранения значения в объекте, имеет тип int, что дает memcpy выход за его пределы.   -  person Some Name    schedule 11.03.2019
comment
@CacahueteFrito да, я знаю, я не был достаточно ясным, я предполагал более общий случай, например, int f1; int f2; int a; long b, чтобы иметь заполнение, пока поле не является первым. Конечно, самый простой способ для компилятора - добавить заполнение непосредственно перед требующим его полем, но является ли это нормой?   -  person bruno    schedule 11.03.2019
comment
@EugeneSh .: В то время как memcpy записывает за пределы s.a, он находится в пределах s, а стандарт C разрешает доступ к байтам объекта (которым является s) в виде массива символьного типа, в том числе через memcpy.   -  person Eric Postpischil    schedule 11.03.2019
comment
@EricPostpischil Но мы использовали lvalue типа int для хранения значения объекта, представление которого превышает sizeof(int). Не вызывает ли это неопределенного поведения?   -  person Some Name    schedule 11.03.2019
comment
@SomeName: int * не использовался для хранения значения. int * использовался в выражении, которое было аргументом для memcpy, но семантика вызовов функций заставляет его преобразовывать в void *. Затем, согласно спецификации memcpy, он копирует символы из одного места в другое. Есть некоторая педантичная сложность в том, можно ли использовать int *, равный &s.a, для доступа к другим байтам s. Я готовлю ответ, который может решить эту проблему.   -  person Eric Postpischil    schedule 11.03.2019
comment
УБ? Хммм ... memset(&s, 0, sizeof s) действительно. Так что я сомневаюсь, что memset(&s.a, 0, sizeof s) - это UB.   -  person 4386427    schedule 11.03.2019
comment
@EricPostpischil Хотя это правда, что вы можете memcpy a struct, это должно быть сделано с помощью его указателя &s, а не указателя на член, насколько мне известно. Указатели очень разборчивы. Я это проверю.   -  person alx    schedule 11.03.2019
comment
@ 4386427 Обратите внимание, что вопрос касается строки, в которой переданный размер не совпадает с размером заостренного объекта.   -  person Eugene Sh.    schedule 11.03.2019
comment
@bruno Заполнение может идти куда угодно, кроме первого члена. Без правил.   -  person alx    schedule 11.03.2019
comment
@EugeneSh. да, заметил. Но пока размер меньше / равен sizeof s Я сомневаюсь, что это может быть UB .... но я не могу процитировать стандарт ... и я могу ошибаться, конечно :-)   -  person 4386427    schedule 11.03.2019
comment
@ 4386427 Но в коде не меньше, а больше (в случае заполнения)   -  person Eugene Sh.    schedule 11.03.2019
comment
@EugeneSh. s.a меньше, но s больше. Макет - это int (4 байта), заполнение (4 байта), long (? Байта), поскольку OP не сообщил нам о размере long. Но sizeof s составляет не менее 12 (вероятно, 16 байтов), а код копирует только 8 байтов.   -  person 4386427    schedule 11.03.2019
comment
@ 4386427 Это строка memcpy(&(s.a), repr, sizeof(repr));. Здесь repr - размер s.a плюс отступ s.a. Итак, sizeof(repr) больше sizeof(s.a). Итак, я предполагаю, что вопрос юриста здесь в том, законно ли писать за пределами объекта, который, как известно, является частью более крупного объекта.   -  person Eugene Sh.    schedule 11.03.2019
comment
@EugeneSh. да - согласен - я точно так же написал. Но sizeof(repr) меньше sizeof(s), поэтому код не записывает сторону s.   -  person 4386427    schedule 11.03.2019


Ответы (3)


Что делает вызов memcpy

Вопрос плохо поставлен. Давайте сначала посмотрим на код:

char repr[offsetof(struct first_member_padded_t, b)] = //some value
memcpy(repr, &a, sizeof(a));
memcpy(&(s.a), repr, sizeof(repr));

Сначала обратите внимание, что repr инициализирован, поэтому всем элементам в нем присваиваются значения.

Первый memcpy в порядке - он копирует байты из a в repr.

Если бы второй memcpy был memcpy(&s, repr, sizeof repr);, он скопировал бы байты из repr в s. Это приведет к записи байтов в s.a и, из-за размера repr, в любое заполнение между s.a и s.b. Согласно C 2018 6.5 7 и другим частям стандарта разрешен доступ к байтам объекта (а «доступ» означает как чтение, так и запись согласно 3.1 1). Таким образом, эта копия в s в порядке, и в результате s.a принимает то же значение, что и a.

Однако memcpy использует &(s.a), а не &s. Он использует адрес s.a, а не адрес s. Мы знаем, что преобразование s.a в указатель на тип символа позволит нам получить доступ к байтам s.a (6,5 7 и более) (и передача его в memcpy имеет тот же эффект, что и такое преобразование, поскольку memcpy указывается, чтобы иметь эффект копирования байтов), но неясно, позволяет ли нам получить доступ к другим байтам в s. Другими словами, у нас есть вопрос, можем ли мы использовать &s.a для доступа к байтам, отличным от тех, что указаны в s.a.

6.7.2.1 15 говорит нам, что если указатель на первый член структуры «соответствующим образом преобразован», результат указывает на структуру. Итак, если мы преобразовали &s.a в указатель на struct first_member_padding_t, он будет указывать на s, и мы определенно можем использовать указатель на s для доступа ко всем байтам в s. Таким образом, это также будет хорошо определено:

memcpy((struct first_member_padding t *) &s.a, repr, sizeof repr);

Однако memcpy(&s.a, repr, sizeof repr); преобразует только &s.a в void * (поскольку memcpy объявлен как принимающий void *, поэтому &s.a автоматически преобразуется во время вызова функции), а не в указатель на тип структуры. Это подходящее преобразование? Обратите внимание, что если бы мы сделали memcpy(&s, repr, sizeof repr);, он преобразовал бы &s в void *. 6.2.5 28 сообщает нам, что указатель на void имеет то же представление, что и указатель на символьный тип. Итак, рассмотрим эти два утверждения:

memcpy(&s.a, repr, sizeof repr);
memcpy(&s,   repr, sizeof repr);

Оба этих оператора передают void * в memcpy, и эти два void * имеют то же представление, что и друг друга, и указывают на один и тот же байт. Теперь мы можем интерпретировать стандарт педантично и строго так, чтобы они отличались тем, что последний может использоваться для доступа ко всем байтам s, а первый - нет. Тогда это странно, что у нас есть два обязательно идентичных указателя, которые ведут себя по-разному.

Такая строгая интерпретация стандарта C кажется возможной теоретически - разница между указателями могла возникнуть во время оптимизации, а не в фактической реализации memcpy, - но я не знаю ни одного компилятора, который бы это сделал. Обратите внимание, что такая интерпретация противоречит разделу 6.2 стандарта, в котором говорится о типах и представлениях. Интерпретация стандарта таким образом, что (void *) &s.a и (void *) &s ведут себя по-разному, означает, что две вещи с одним и тем же значением и типом могут вести себя по-разному, что означает, что значение состоит из чего-то большего, чем его значение и тип, что, похоже, не является целью 6.2 или стандарт в целом.

Type-Punning

В вопросе говорится:

Я пытаюсь понять, как работает каламбур, когда дело доходит до сохранения значения в члене структуры или объединения.

Это не каламбур, как обычно используется этот термин. Технически код обращается к s.a, используя lvalue другого типа, чем его определение (потому что он использует memcpy, который определен для копирования, как если бы с символьным типом, в то время как определенный тип - int), но байты происходят из int и являются копируется без изменений, и такое копирование байтов объекта обычно рассматривается как механическая процедура; это делается для создания копии, а не для переинтерпретации байтов в новый тип. «Типаж» обычно относится к использованию разных lvalue с целью переинтерпретации значения, например записи unsigned int и чтения float.

В любом случае, каламбур на самом деле не является предметом вопроса.

Ценности в членах

Заголовок спрашивает:

Какие значения мы можем хранить в членах структуры или объединения?

Этот заголовок не соответствует содержанию вопроса. На вопрос, связанный с заголовком, легко ответить: значения, которые мы можем сохранить в члене, - это те значения, которые может представлять тип члена. Но вопрос идет дальше, чтобы исследовать отступы между участниками. Заполнение не влияет на значения в элементах.

Padding принимает неуказанные значения

Вопрос цитирует стандарт:

Когда значение хранится в объекте типа структуры или объединения, в том числе в объекте-члене, байты представления объекта, соответствующие любым байтам заполнения, принимают неопределенные значения.

и говорит:

Поэтому я интерпретировал это так, как будто у нас есть объект для хранения в члене, так что размер объекта равен sizeof(declared_type_of_the_member) + padding байты, связанные с заполнением, будут иметь неопределенное значение ...

Цитируемый текст в стандарте означает, что если байты заполнения в s были установлены на некоторые значения, как в случае с memcpy, а затем мы выполняем s.a = something;, то байты заполнения больше не требуются для хранения своих предыдущих значений.

Код в вопросе исследует другую ситуацию. Код memcpy(&(s.a), repr, sizeof(repr)); не сохраняет значение в элементе структуры в смысле, обозначенном в 6.2.6.1 6. Он не сохраняется ни в одном из элементов s.a или s.b. Это копирование байтов в, что отличается от того, что обсуждается в 6.2.6.1.

6.2.6.1 6 означает, что, например, если мы выполним этот код:

char repr[sizeof s] = { 0 };
memcpy(&s, repr, sizeof s); // Set all the bytes of s to known values.
s.a = 0; // Store a value in a member.
memcpy(repr, &s, sizeof s); // Get all the bytes of s to examine them.
for (size_t i = sizeof s.a; i < offsetof(struct first_member_padding_t, b); ++i)
    printf("Byte %zu = %d.\n", i, repr[i]);

тогда не обязательно, чтобы все нули были напечатаны - байты в заполнении могли измениться.

person Eric Postpischil    schedule 11.03.2019
comment
Как вы говорите, оба указателя будут иметь одинаковое представление, но все же они не указывают на одно и то же, поэтому, как вы также сказали, компилятор может выполнять оптимизацию на основе этого. - person alx; 11.03.2019
comment
Кроме того, в более общем случае элемента struct, который не является его первым элементом, будет более ясно, что это UB. - person alx; 11.03.2019
comment
@CacahueteFrito: Re «они не указывают на одно и то же»: они указывают на одно и то же; (char *) &s и (char *) &s.a должны оба указывать на первый байт s.a, а также (void *) &s и (void *) &s.a. Если стандарт интерпретируется таким образом, что между ними существует различие, это различие должно происходить из-за их происхождения, а не из того, на что они указывают. Это означает, что значение в C состоит не только из его типа и значения, что противоречит разделу «Концепции» (6.2) стандарта C. - person Eric Postpischil; 11.03.2019
comment
@CacahueteFrito: использование члена, отличного от первого, не влияет на анализ. У нас была бы такая же ситуация с (void *) &s.b и (void *) ((char *) &s + offsetof(struct first_member_padding_t, b)) - первый указывает на то же место, что и второй, и они того же типа с тем же представлением, но первый является производным от указателя на член, а второй - от указателя. к структуре. - person Eric Postpischil; 11.03.2019
comment
Теперь мы можем интерпретировать стандарт педантично и строго так, чтобы они отличались тем, что последний может использоваться для доступа ко всем байтам s, а первый - нет. Но в стандарте содержится информативная заметка, что 48) The same representation and alignment requirements are meant to imply interchangeability as arguments to functions, return values from functions, and members of unions. Итак, если мы скажем, что &s может использоваться для доступа ко всем членам (в этом случае через memcpy), но &(s.a) не может, это будет противоречить информативному примечанию. Не так ли? - person Some Name; 12.03.2019
comment
Использование другого члена, кроме первого, не меняет анализа. Не могли бы вы уточнить это? Согласно 7.24.2.1(p1) Функция memcpy копирует n символов из объекта, на который указывает s2, в объект, на который указывает s1. При рассмотрении первого члена мы можем полагаться на 6.7.2.1(p15) и использовать указатель на него и указатель на всю структуру взаимозаменяемо. При рассмотрении второго члена у нас нет такой взаимозаменяемости с указателем на весь объект структуры, поэтому мы ограничиваемся рассмотрением объекта члена как такового. - person Some Name; 12.03.2019
comment
@SomeName: (void *) ((char *) &s + offsetof(struct first_member_padding_t, b)) указывает на первый байт s.b. Это указатель, полученный в результате преобразования &s в char *, и мы можем использовать этот указатель для доступа к s, как если бы это был массив байтов, перемещая байты s вверх и вниз по желанию. Теперь (void *) &s.b также является указателем на первый байт s.b, и это void *, как и первый указатель. Итак, у нас есть два указателя одного типа и представления, которые указывают на один и тот же байт, но мы не можем, согласно педантичной интерпретации, использовать их как взаимозаменяемые. - person Eric Postpischil; 12.03.2019
comment
@EricPostpischil @SomeName Я продолжу использовать массив схожести массивов (оба являются большими объектами, которые содержат более мелкие объекты и имеют одинаковую позицию в ОЗУ): Давайте возьмем int a[2][2][2][2];. Тогда (void *)a == (void *)(a[0][0][0]) это true, но я думаю (и по этому поводу уже есть старый вопрос, но я не могу его найти) вы не можете memcpy(dest, (a[0][0][0]), 8); - person alx; 12.03.2019
comment
@CacahueteFrito: Вы цитировали какую-либо часть стандарта C или приводили ли какие-либо доводы в поддержку этого утверждения? - person Eric Postpischil; 12.03.2019
comment
@EricPostpischil Это проблема доступа к массиву вне пределов. Однако, какой массив следует рассматривать (внутренний или внешний), в Стандарте не указано, поэтому его нельзя цитировать. Я обращусь к ответу здесь (я наконец нашел его сегодня утром): stackoverflow.com/a/51738580/6872717 - person alx; 12.03.2019
comment
@EricPostpischil Раздел 6.5 6 определяет Если значение копируется в объект, не имеющий объявленного типа, с использованием memcpy или memmove, или копируется как массив символьного типа. Меня смущает скопированный массив символьного типа, означает ли это, что мы сначала копируем представление объекта в какой-то char[n] (как в моем случае), а затем копируем его рядом с конечным пунктом назначения? - person Some Name; 12.03.2019
comment
@CacahueteFrito: правила арифметики указателей вне пределов отличаются от правил доступа к объектам. После того как указатель преобразован из указателя на элемент массива в какой-либо другой тип, правила арифметики указателя, относящиеся к этому конкретному массиву, не имеют значения. Вопросы о том, можно ли использовать указатель на подобъект для доступа к объекту, обсуждаемые в предоставленной вами ссылке, такие же, как и в этом ответе. - person Eric Postpischil; 12.03.2019
comment
@SomeName: «Скопировано как массив символьного типа» по сути означает побайтовое копирование с использованием указателя на символьный тип. Однако это предложение о вещах без объявленного типа относится к выделенной памяти. В примере в вашем вопросе используется объект с объявленным типом, объект s с объявленным типом struct first_member_padded_t. - person Eric Postpischil; 12.03.2019

Во многих реализациях языка C Standard был написан для описания, попытка записать N-байтовый объект в структуре или объединении может повлиять на значение не более N байтов в структуре или объединении. С другой стороны, на платформе, которая поддерживает 8-битные и 32-битные хранилища, но не 16-битные хранилища, если кто-то объявил такой тип, как:

struct S { uint32_t x; uint16_t y;} *s;

а затем выполнили s->y = 23;, не заботясь о том, что произошло с двумя байтами, следующими за y, было бы быстрее выполнить 32-битное сохранение в y, слепо перезаписав два следующих за ним байта, чем выполнить пару 8-битных записей для обновления верхняя и нижняя половины y. Авторы Стандарта не хотели запрещать такое обращение.

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

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

person supercat    schedule 11.03.2019

Как сказал @ user694733, в случае заполнения между s.a и s.b, memcpy() обращается к области памяти, к которой не может получить доступ &a:

int a = 1;
int b;
b = *((char *)&a + sizeof(int));

Это неопределенное поведение, и в основном это то, что происходит внутри memcpy().

person alx    schedule 11.03.2019
comment
Я не согласен, это не тот же случай, ваши переменные не являются полями части структуры, вы не можете применить offsetof, не так ли? - person bruno; 11.03.2019
comment
Вы не можете, но тот факт, что указатель на переменную не может использоваться (с помощью арифметики указателей) для доступа к памяти, которая не принадлежит этой переменной, все еще сохраняется. Этот пример показывает ту же проблему, но убирает struct, чтобы было проще понять. - person alx; 11.03.2019
comment
Вы можете воспроизвести тот же UB с массивом массивов: int a[2][2]; /*...*/ x = *(&(a[0][0]) + 2); также UB. - person alx; 11.03.2019
comment
Не думаю, что с struct и без него то же самое - person 4386427; 11.03.2019
comment
Если кто-то не обнаружит разницы, в этом случае и массив массивов, и struct должны вести себя одинаково. В случае массива массивов, если у вас есть указатель на элемент одного из массивов, вы можете получить доступ к любому элементу в этом массиве, но вы не можете перейти к следующему. - person alx; 11.03.2019
comment
@CacahueteFrito: Есть много ситуаций, когда у программы может быть только один полезный способ поведения, но Стандарт не прилагает никаких усилий, чтобы запретить соответствующим реализациям вести себя глупо-бесполезным образом. На языке, для описания которого был написан Стандарт, функцию memcpy можно было использовать для любой комбинации объектов в непрерывно выделенной области хранения, независимо от границ каких-либо внутренних массивов. Любая неспособность Стандарта учесть такие случаи будет означать разницу между языком, для описания которого он был написан, и языком, на котором он написан. - person supercat; 11.03.2019
comment
@supercat Я согласен с вами, что нет смысла запрещать это, если только это не позволит оптимизировать, что приветствуется, и тогда я бы согласился. Но все же хорошо знать, что Стандарт (хороший или нет) позволяет, потому что тогда, когда вы сталкиваетесь с ошибкой, используя -O3, вы можете сказать, ага, это та сумасшедшая вещь, от которой без ума Стандарт, и не терять много времени на отладку кода выглядит нормально. Также хорошо, что будущие версии стандарта могут это исправить. (Если бы этого никто не знал, как бы вы это исправить?) - person alx; 12.03.2019
comment
@CacahueteFrito: Согласно авторам Стандарта, две фундаментальные части Духа C - это Доверять программисту и Не мешать программисту делать то, что нужно сделать. Поскольку разные задачи требуют умения делать разные вещи, авторы хотели поощрять разнообразие реализаций, предназначенных для разных целей. Если реализация утверждает, что подходит для низкоуровневого программирования, не требуя использования нестандартного синтаксиса (который раньше был общей целью, пока по какой-то причудливой причине он не стал немодным) ... - person supercat; 12.03.2019
comment
... это означало бы, что он должен надежно обрабатывать конструкции, которые могут не понадобиться для других типов программирования, и невыполнение этого должно считаться ошибкой независимо от того, приведет ли это к несоответствующей реализации . Жаль, что нет общей терминологии, позволяющей отличить язык, для описания которого был написан Стандарт, от выпотрошенной оболочки языка, которую разработчики кричат, а gcc рассматривает ее как определяющую. - person supercat; 12.03.2019
comment
@CacahueteFrito: Что касается оптимизации, которая приветствуется, это зависит от того, соответствуют ли они духу C применительно к текущей задаче. Часто некоторые части стандарта или документации реализации описывают поведение действия, но в другой части описывается перекрывающийся класс действий как вызывающий неопределенное поведение. Оптимизация, основанная на том, что такие действия являются UB, может приветствоваться в тех случаях, когда такие действия будут бесполезны, но контрпродуктивны в случаях, когда такие действия представляют собой наиболее эффективный способ выполнения поставленной задачи. - person supercat; 12.03.2019