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

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

Стандарт 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 для такой программы?Я думаю, что это так.

Ответы [ 3 ]

3 голосов
/ 11 марта 2019

Что делает memcpy Calls

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

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 имеет тот же эффект, что и такое преобразование, как * Указано, что 1035 * влияет на копирование байтов), но неясно, что позволяет нам получить доступ к другим байтам в 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 или стандарт вообще.

Тип-каламбурная

Вопрос гласит:

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

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

В любом случае наложение типа на самом деле не являетсяТема вопроса.

Значения в элементах

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

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

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

Заполнение принимает неопределенные значения

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

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

и говорят:

ИтакЯ интерпретировал это так, как будто у нас есть объект для хранения в элементе, так что размер объекта равен s izeof(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]);

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

1 голос
/ 11 марта 2019

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

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

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

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

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

0 голосов
/ 11 марта 2019

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

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

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

...