Четко ли определен доступ к членам через offsetof?

При выполнении арифметических действий с указателем с помощью offsetof является ли хорошо определенным поведением получение адреса структуры, добавление к ней смещения члена, а затем разыменование этого адреса для перехода к базовому члену?

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

#include <stddef.h>
#include <stdio.h>

typedef struct {
    const char* a;
    const char* b;
} A;

int main() {
    A test[3] = {
        {.a = "Hello", .b = "there."},
        {.a = "How are", .b = "you?"},
        {.a = "I\'m", .b = "fine."}};

    for (size_t i = 0; i < 3; ++i) {
        char* ptr = (char*) &test[i];
        ptr += offsetof(A, b);
        printf("%s\n", *(char**)ptr);
    }
}

Это должно печатать «там», «ты?» и "хорошо". в трех последовательных строках, что в настоящее время выполняется как с clang, так и с gcc, поскольку вы можете проверить себя на wandbox. Однако я не уверен, нарушают ли какие-либо из этих приведения указателей и арифметические действия какое-либо правило, которое может привести к тому, что поведение станет неопределенным.


person Ben Steffan    schedule 02.10.2017    source источник
comment
Мне очень интересно, почему вы хотите сделать что-то подобное? Это простое любопытство? Или есть какая-то основная проблема, которую вы хотите решить таким образом? Если второе, то, возможно, вам следует спросить об этом?   -  person Some programmer dude    schedule 02.10.2017
comment
@Someprogrammerdude В основном из любопытства. Идея просто возникла при написании кода, и это действительно может означать (очень небольшую) оптимизацию, но здесь нет реальной проблемы, которую я пытаюсь решить. Я особенно заинтересован в этом offsetof использовании.   -  person Ben Steffan    schedule 02.10.2017
comment
Когда вы упоминаете оптимизацию, у меня в голове звенит тревожный звоночек. Не выполняйте преждевременную оптимизацию, вместо этого пишите в первую очередь простой, читаемый и, что наиболее важно, поддерживаемый код. Затем помните, что достаточно хорошо часто достаточно хорошо. И только если производительность вашей программы недостаточно хороша для ваших требований, вы измеряете, профилируете и тестируете, чтобы найти узкие места и исправить только наихудшие из них (с большим количеством комментариев и документации).   -  person Some programmer dude    schedule 02.10.2017
comment
Вы нарушаете строгое правило псевдонимов.   -  person Klas Lindbäck    schedule 02.10.2017
comment
@Someprogrammerdude Я хорошо осознаю опасность преждевременной оптимизации. Возможно, я недостаточно ясно выразился, но на самом деле я не собираюсь использовать это, если только это не требуется из-за дефицита производительности и т. д. Это просто контекст, в котором я придумал эту идею. Я задал этот вопрос, потому что мне искренне интересно, возможно ли это на самом деле или нет, а не потому, что я собираюсь его использовать.   -  person Ben Steffan    schedule 02.10.2017
comment
@BenSteffan Я даже не уверен, что в любом случае выполнение этого вручную может быть какой-либо оптимизацией. Использование прямого доступа к членам с test[i].b будет выполнять ту же арифметику указателя, что и необходимо, но гарантирует правильное поведение, в то время как взлом указателя - нет. И поскольку это распространенный случай, именно на нем разработчики компиляторов сосредоточат внимание на оптимизации, в то время как у них могут быть не столь эффективно оптимизированные хаки с указателями. Можно представить себе случай, когда test[i].b компилируется в одну инструкцию load, а ручная арифметика выполняется в виде отдельных шагов, за которыми следует load.   -  person zstewart    schedule 03.10.2017
comment
@zstewart: Если несколько структур будут иметь определенные поля в одних и тех же местах, и вам нужна функция, которая может взаимозаменяемо работать со всеми такими структурами, я не уверен, как можно написать код без использования offsetof или компилятора, который фактически обрабатывает Common Initial Sequence гарантирует удобство использования.   -  person supercat    schedule 04.10.2017
comment
@supercat о, интересно. У меня сложилось неправильное впечатление о возможности приведения структур с общими префиксами, потому что я думал, что это делает Python, а они были но похоже, что их больше нет.   -  person zstewart    schedule 05.10.2017
comment
@zstewart: возможность приведения между структурами с общими префиксами разрешена стандартом, если полное объявление типа объединения, содержащее оба типа, видно при доступе к структуре, но авторы gcc утверждают, что правило гласит, что требуется только соблюдать правило CIS, когда все обращения к lvalue выполняются через тип union, даже если это не то, что говорит правило, и сделало бы правило бесполезным.   -  person supercat    schedule 05.10.2017


Ответы (2)


Насколько я могу судить, это четко определенное поведение. Но только потому, что вы обращаетесь к данным через тип char. Если бы вы использовали какой-либо другой тип указателя для доступа к структуре, это было бы «строгим нарушением псевдонимов».

Строго говоря, обращение к массиву за пределами границ определено нечетко, но правильно определено использование указателя символьного типа для извлечения любого байта из структуры. Используя offsetof, вы гарантируете, что этот байт не является байтом заполнения (что могло бы означать, что вы получите неопределенное значение).

Однако обратите внимание, что отбрасывание квалификатора const приводит к плохо определенному поведению.

РЕДАКТИРОВАТЬ

Точно так же приведение (char**)ptr является недопустимым преобразованием указателя - само по себе это неопределенное поведение, поскольку оно нарушает строгое сглаживание. Сама переменная ptr была объявлена ​​как char*, поэтому вы не можете солгать компилятору и сказать: «Эй, это на самом деле char**», потому что это не так. Это не зависит от того, на что указывает ptr.

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

#include <stddef.h>
#include <stdio.h>
#include <string.h>

typedef struct {
    const char* a;
    const char* b;
} A;

int main() {
    A test[3] = {
        {.a = "Hello", .b = "there."},
        {.a = "How are", .b = "you?"},
        {.a = "I\'m", .b = "fine."}};

    for (size_t i = 0; i < 3; ++i) {
        const char* ptr = (const char*) &test[i];
        ptr += offsetof(A, b);

        /* Extract the const char* from the address that ptr points at,
           and store it inside ptr itself: */
        memmove(&ptr, ptr, sizeof(const char*)); 
        printf("%s\n", ptr);
    }
}
person Lundin    schedule 02.10.2017
comment
Не уверен, почему вы сделали это приведение, так как это лишнее. -- Актерский состав имеет смысл. ptr имеет тип char * и указывает на первый байт значения указателя. Его необходимо привести к типу «указатель на указатель», чтобы получить доступ к фактическому значению указателя. Вы можете возразить, что это должно быть const char **, а не char **, но там определенно должно быть приведение. - person ; 02.10.2017
comment
@hvd Достаточно честно, это не лишнее, это обычный UB. Преобразование char* в char** является недопустимым преобразованием указателя, даже если эффективным типом данных, на которые указывает указатель, оказался указатель. - person Lundin; 02.10.2017
comment
Предполагается, что преобразование char * в char ** допустимо, когда указываемое значение действительно является char *. В противном случае весь макрос offsetof было бы довольно сложно правильно использовать (как указывает @cmaster, это все еще возможно). Где стандарт говорит, что это недопустимое преобразование указателя? 6.3.2.3p7, по крайней мере, говорит, что задано char *p;, char *q = (char *) &p; допустимо, и после этого должно выполняться (char **) q == &p. - person ; 02.10.2017
comment
@Lundin Итак, вы думаете, что замена приведения (char**)ptr на эквивалентное char* string; memcpy(&string, ptr, sizeof(string)); сделает фрагмент кода четко определенным? - person cmaster - reinstate monica; 02.10.2017
comment
@hvd 6.3.2.3/7 указывает, какое преобразование возможно в отношении синтаксиса указателя - C допускает все виды приведения диких указателей. Можете ли вы на самом деле получить доступ к этим указанным данным или нет, определяется эффективным типом/строгим псевдонимом, 6.5/6 и 6.5/7. Ну и эффективный тип объекта const char*. Так что я полагаю, что единственная проблема, в конце концов, это const. Если ptr является указателем на такой объект, то правильный тип для использования будет char *const *. Пока char** несовместимый тип. - person Lundin; 02.10.2017
comment
@cmaster Помимо проблемы с константой, тогда да, этот код также будет четко определенным. - person Lundin; 02.10.2017
comment
Хм, если подумать... Проблема, в которой я не был уверен, заключается в самом ptr, а не в указанных данных. ptr явно является char* в исходном коде, вы не можете сказать компилятору, что ptr на самом деле является char**. char** не может быть псевдонимом char* - это фактическая проблема! Не указанные данные. Я обновлю ответ. - person Lundin; 02.10.2017
comment
@Lundin Оба, правда. 6.3.2.3p7 требуется, чтобы определить, указывает ли преобразованный указатель на предполагаемый объект, затем требуется 6.5, чтобы определить, разрешен ли доступ к этому объекту правилами псевдонимов. Что касается вашего обновления, вы не можете солгать компилятору и сказать, что это на самом деле char** - код этого не делает. (char***)&ptr будет делать это, и это действительно будет недопустимо, но (char**)ptr только преобразует значение, хранящееся в ptr, в char**, оно не делает вид, что ptr является char**. - person ; 02.10.2017
comment
@hvd ptr, будучи char*, может указывать только на символы, а не на другие указатели. Конечно, в двоичном файле есть указатель b, но компилятор может предположить, что строка(char**)ptr не используется для доступа к указателю b, поскольку char** нельзя использовать для доступа к char*. Независимо от вопроса const. - person Lundin; 02.10.2017
comment
@hvd Вы можете убедиться, что это действительно UB, написав b с помощью этого метода char** в различных компиляторах, а затем распечатав значение b после этого. Возможно, значение не было обновлено из-за строгого нарушения псевдонимов. Вероятнее всего, неправильно себя ведет компилятор gcc, потому что он агрессивно оптимизирует такой код. - person Lundin; 02.10.2017
comment
@Lundin ptr, будучи char*, может указывать только на символы, а не на другие указатели. -- Существует специальное исключение для типов символов. Они могут использоваться для указания и доступа к любому объекту. Это то, что 6.3.2.3p7 охватывает в последних двух предложениях, и для чего предназначено специальное исключение в 6.5p7 (тип символа). И это хорошо поддерживается даже в GCC. - person ; 02.10.2017
comment
@hvd Да, я прекрасно знаю. Вы можете использовать его для считывания двоичного представления любого типа данных (в этом случае вам действительно понадобится unsigned char или uint8_t). Но это не то, как это было использовано здесь! Для этого вам придется создать какое-нибудь непереносимое чудовище, такое как const char* p = ptr[0] << 24 | ptr[1] << 16, которое предполагает, что вы знаете конкретное представление указателя и его порядок следования байтов. - person Lundin; 02.10.2017
comment
@Lundin Или, как в некоторых случаях позволяет 6.3.2.3p7, просто приведите значение char * обратно к указателю на правильный тип, в данном случае const char **, и разыменуйте его. Что и делает ОП. Вы только что заявили, что это неверно, потому что char * не может указывать на объекты-указатели, только на символы, но теперь вы, кажется, признаете, что разрешено как указывать на другие объекты, так и обращаться к другим объектам. - person ; 02.10.2017
comment
@hvd Правильный тип ptr в вопросе - char* и ничего больше, и точка. Как я уже сказал, не имеет значения, на что он указывает, потому что компилятор предположит, что данные, на которые он указывает, какими бы они ни были, не будут доступны через char**. У строгого правила псевдонимов есть исключение для сериализации двоичных данных байт за байтом, и это был пример, который я только что привел, поскольку вы думали, что он применим здесь, но он не имеет никакого отношения к случаю OP. - person Lundin; 02.10.2017
comment
@Lundin Нет, компилятор не будет этого предполагать, потому что нигде в стандарте нет правила, позволяющего компилятору предполагать это. В случае OP нет доступа к члену b с использованием lvalue char, потому что ptr никогда не разыменовывается напрямую, и даже если член b был разыменован напрямую, всегда применяется исключение для типов символов , поскольку стандарт не делает и не может делать исключений в зависимости от того, что думает программист в данный момент, он не может применяться только тогда, когда целью является сериализация. - person ; 02.10.2017
comment
@Lundin: наблюдение за тем, что делают компиляторы, не является эффективным способом проверки того, определено ли что-либо Стандартом. Знание того, что компилятор обрабатывает определенные конструкции фиктивным образом, может быть полезным, но это не означает, что Стандарт не определяет эти конструкции. - person supercat; 04.10.2017

Данный

struct foo {int x, y;} s;
void write_int(int *p, int value) { *p = value; }

ничто в Стандарте не различает:

write_int(&s.y, 12); //Just to get 6 characters

а также

write_int((int*)(((char*)&s)+offsetof(struct foo,y)), 12);

Стандарт может быть прочитан таким образом, чтобы подразумевать, что оба вышеприведенных пункта нарушают правила типа lvalue, поскольку в нем не указано, что к хранимому значению структуры можно получить доступ с использованием lvalue типа члена, требуя, чтобы код, желающий для доступа в качестве члена структуры записывается как:

void write_int(int *p, int value) { memcpy(p, value, sizeof value); }

Я лично думаю, что это нелепо; если &s.y нельзя использовать для доступа к lvalue типа int, почему оператор & дает int*?

С другой стороны, я также не думаю, что имеет значение то, что говорит Стандарт. Ни clang, ни gcc не могут быть уверены в правильной обработке кода, который делает что-то «интересное» с указателями, даже в случаях, однозначно определенных Стандартом, за исключением случаев, когда они вызываются с помощью -fno-strict-aliasing. Компиляторы, которые прилагают добросовестные усилия, чтобы избежать любых неправильных «оптимизаций» псевдонимов в случаях, которые были бы определены по крайней мере в некоторых правдоподобных чтениях Стандарта, не будут иметь проблем с обработкой кода, который использует offsetof в случаях, когда все доступы, которые будут выполняться с использованием указатель (или другие производные от него указатели) предшествуют следующему доступу к объекту другими средствами.

person supercat    schedule 03.10.2017
comment
Вы бы рекомендовали всегда использовать -fno-strict-aliasing? Мне кажется, это единственный разумный выход. И знаете ли вы, все ли современные компиляторы предлагают подобный флаг? Если нет, то какие еще есть варианты? - person GermanNerd; 04.10.2019
comment
@GermanNerd: Люди, желающие продавать компиляторы, разрабатывают их для удовлетворения потребностей клиентов. Возможно, что некоторые специализированные компиляторы могут не предлагать опции для отключения тупых оптимизаций, которые нарушают общепринятые конструкции, но любой качественный компилятор, предназначенный для низкоуровневого программирования, предложит режим, который будет обрабатывать конструкции, которые могут оптимизаторы gcc/clang. т. Для внесения чего-либо полезного в Стандарт потребуется консенсус между тремя фракциями: теми, кто хочет разрешить агрессивную оптимизацию с помощью реализаций, предназначенных для целей, в которых они будут в порядке,... - person supercat; 04.10.2019
comment
... те, кто хочет иметь возможность использовать низкоуровневые конструкции в случаях, когда они требуются, и те, кто выступает против фрагментации языка, признавая, что реализации, предназначенные для некоторых целей, должны предлагать семантику, которая не может быть практически поддержана теми, кто предназначен для каких-то других целей. ИМХО, Стандарт был бы наиболее полезен, если бы можно было исключить третью группу, но Стандарт, который исключает одну из остальных, и признается, что не предпринимает никаких усилий для удовлетворения потребностей этой группы, был бы лучше чем то, что мы имеем сейчас. - person supercat; 04.10.2019
comment
Спасибо за ваши разработки. Я лично считаю, что введение ограничений на язык для облегчения оптимизации — это катастрофа, полностью противоречащая духу C. Не говоря уже о том, что SAR в основном приглашает программистов попасть в тщательно подготовленные ловушки… поведение, которое можно отследить только в том случае, если вы понимаете конкретную (!) оптимизацию компилятора. Это должно быть портативно? Чтобы быть более конкретным: если я выделяю некоторую память (malloc), я ожидаю, что эта память будет МОЕЙ, чтобы делать с ней все, что я считаю нужным. Это может быть очень полезно... - person GermanNerd; 04.10.2019
comment
@GermanNerd: N1570 6.5p7 подойдет, если не считать одного слова: by. Если бы это было заменено на ... с видимой связью с ..., в сноске было ясно указано, что способность распознавать отношения является проблемой качества реализации, правило было бы в порядке. Я разделяю ваше мнение о вандалах, крадущих язык. - person supercat; 04.10.2019
comment
Давайте продолжим это обсуждение в чате. - person GermanNerd; 04.10.2019