Подтвердить что ты не робот

В операторе запятой левый операнд гарантированно не выполняется, если он не имеет побочных эффектов?

Чтобы показать тему, я собираюсь использовать C, но тот же макрос можно использовать также в С++ (с или без struct), поднимая тот же вопрос.

Я придумал этот макрос

#define STR_MEMBER(S,X) (((struct S*)NULL)->X, #X)

Его цель состоит в том, чтобы иметь строки (const char*) существующего члена struct, так что, если член не существует, компиляция не выполняется. Минимальный пример использования:

#include <stdio.h>

struct a
{
    int value;
};

int main(void)
{
    printf("a.%s member really exists\n", STR_MEMBER(a, value));
    return 0;
}

Если value не был членом struct a, код не будет компилироваться, и это то, что я хотел.

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

В этом случае, однако, нет (предполагаемых) побочных эффектов, но, конечно, это работает, если компилятор фактически не производит код, который оценивает выражение, поскольку в противном случае он имел бы доступ к struct, расположенному при NULL и произойдет ошибка сегментации.

Gcc/g++ 6.3 и 4.9.2 никогда не приводили этот опасный код даже при -O0, как если бы они всегда могли "видеть", что оценка не имеет побочных эффектов и поэтому ее можно пропустить.

Добавление volatile в макрос (например, поскольку доступ к этому адресу памяти является желательным побочным эффектом) был единственным способом вызвать ошибку сегментации.

Итак, вопрос: есть что-либо в стандартах на языках C и С++, что гарантирует, что компиляторы всегда будут избегать фактической оценки левого операнда оператора запятой, когда компилятор может быть уверен, что оценка не имеет стороны эффекты?

Примечания и фиксация

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

У меня уже есть два очевидных решения: "reifying" struct и использование offsetof. Первый требует доступной области памяти, такой большой, как самый большой struct, который мы используем в качестве первого аргумента STR_MEMBER (например, возможно, статический союз мог бы...). Последний должен работать безупречно: он дает смещение, которого мы не интересуем и избегаем проблемы с доступом - действительно, я предполагаю gcc, потому что это компилятор, который я использую (отсюда и тег), и что его встроенный offsetof ведет себя.

При исправлении offsetof макрос становится

#define STR_MEMBER(S,X) (offsetof(struct S,X), #X)

Запись volatile struct S вместо struct S не вызывает segfault.

Также можно приветствовать предложения о других возможных "исправлениях".

Добавлено примечание

Собственно, реальный случай использования был в С++ в статическом хранилище struct. Кажется, что это хорошо на С++, но как только я попробовал C с кодом, близким к оригиналу, а не с кипяченой для этого вопроса, я понял, что C совсем не доволен этим:

error: initializer element is not constant

C хочет, чтобы структура была инициализирована во время компиляции, вместо этого С++ это хорошо с этим.

4b9b3361

Ответ 1

Оператор запятой (документация по C, говорит что-то очень похоже) не имеет таких гарантий.

В выражении с запятой E1, E2 выражение E1 оценивается, его результат отбрасывается... и его побочные эффекты завершаются до того, как начнется оценка выражения E2

нерелевантная информация опущена

Проще говоря, будет оценен E1, хотя компилятор может оптимизировать его с помощью правила as-if, если он может определить, что побочных эффектов нет.

Ответ 2

Есть ли что-нибудь в стандартах на языках C и С++, гарантирующих, что компиляторы всегда будут избегать фактической оценки левого операнда оператора запятой?

Это наоборот. Стандарт гарантирует, что левый операнд IS оценен (действительно, он не имеет никаких исключений). Результат отбрасывается.


Примечание: для выражений lvalue "оценка" не означает "доступ к сохраненному значению". Вместо этого это означает, что нужно определить, где находится указанное место памяти. Другой код, включающий выражение lvalue, может или не может продолжаться, чтобы получить доступ к ячейке памяти. Процесс чтения из ячейки памяти известен как "преобразование lvalue" в C или "lvalue to rvalue conversion" в С++.

В С++ выражение с отбрасываемым значением (например, левый операнд оператора запятой) имеет только lvalue для выполнения преобразования rvalue, если оно volatile, а также соответствует некоторым другим критериям (см. С++ 14 [expr ]/11 для подробностей). В C lvalue выполняется преобразование для выражений, результат которых не используется (C11 6.3.2.1/2).

В вашем примере это вопрос, происходит ли преобразование lvalue. В обоих языках X->Y, где X - указатель, определяется как (*X).Y; в C акт применения * к нулевому указателю уже вызывает поведение undefined (C11 6.5.3/3), а в С++ оператор . определяется только для случая, когда левый операнд фактически обозначает объект (С++ 14 [expr.ref]/4.2).

Ответ 3

Gcc/g++ 6.3 и 4.9.2 никогда не приводили этот опасный код даже с -O0, как если бы они всегда могли "видеть", что оценка не имеет побочных эффектов, и поэтому ее можно пропустить.

clang будет генерировать код, который вызывает ошибку, если вы передадите ему параметр -fsanitize=undefined. Который должен ответить на ваш вопрос: по крайней мере один крупный разработчик проекта явно рассматривает код как имеющий поведение undefined. И они верны.

Также можно приветствовать предложения о других возможных "исправлениях".

Я бы искал что-то, что гарантировано не оценивать выражение. Ваше предложение offsetof выполняет задание, но может иногда приводить к отклонению кода, который в противном случае был бы принят, например, когда X есть a.b. Если вы хотите, чтобы это было принято, моя мысль заключалась бы в использовании sizeof, чтобы заставить выражение оставаться неудовлетворительным.

Ответ 4

Вы спрашиваете,

есть ли что-либо в стандарте на языках C и С++, который гарантирует что компиляторы всегда будут избегать фактической оценки левого операнда оператора запятой, когда компилятор может быть уверен, что оценка не имеет побочных эффектов?

Как отмечали другие, ответ "нет". Напротив, стандарты безоговорочно утверждают, что левый операнд оператора запятой оценивается и что результат отбрасывается.

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

Что касается его исправления, у вас есть различные варианты, некоторые из которых применяются только к одному или другому из двух названных вами языков. Мне нравится ваша альтернатива offsetof(), но другие отметили, что в С++ существуют типы, к которым offsetof не может применяться. В С, с другой стороны, стандарт конкретно описывает свое приложение для типов структуры, но ничего не говорит о типах союзов. Его поведение по типам профсоюзов, хотя очень вероятно, будет последовательным и естественным, как технически undefined.

Только в C вы можете использовать составной литерал, чтобы избежать поведения undefined в вашем подходе:

#define HAS_MEMBER(T,X) (((T){0}).X, #X)

Это одинаково хорошо работает в структурах и типах соединений (хотя вам нужно предоставить полное имя типа для этой версии, а не только тег). Его поведение хорошо определено, когда данный тип имеет такой член. Расширение нарушает языковое ограничение - поэтому требуется, чтобы диагностика была испускана - когда тип не имеет такого члена, в том числе, когда он не является ни структурным типом, ни типом объединения.

Вы также можете использовать sizeof, как предположил @alain, потому что, хотя выражение sizeof будет оценено, его операнд не будет оцениваться (кроме, в C, когда его операнд имеет измененный тип, который будет не применяются к вашему использованию). Я думаю, что эта вариация будет работать как на C, так и на С++ без введения какого-либо поведения undefined:

#define HAS_MEMBER(T,X) (sizeof(((T *)NULL)->X), #X)

Я снова написал его так, чтобы он работал как для структур, так и для объединений.

Ответ 5

Левый операнд оператора запятой - это выражение с отброшенным значением

5 выражений
11 В некоторых контекстах выражение появляется только для его побочных эффектов. Такое выражение называется отброшенным значением выражение. Выражение оценивается и его значение отбрасывается. [...]

Также есть неопытные операнды, которые, как следует из названия, не оцениваются.

8 В некоторых контекстах отображаются неоцененные операнды (5.2.8, 5.3.3, 5.3.7, 7.1.6.2). Неопределенный операнд не оценивается. Неоценимый операнд считается полным выражением. [...]

Использование выражения с отброшенным значением в вашем примере использования - это поведение undefined, но использование неопытного операнда - это не.

Использование sizeof, например, не вызовет UB, потому что он принимает неоцененный операнд.

#define STR_MEMBER(S,X) (sizeof(S::X), #X)

sizeof предпочтительнее offsetof, потому что offsetof не может использоваться для статических членов и классов, которые не являются стандартными макетами:

18 Библиотека поддержки языков
4 Макрос offset (тип, член-указатель) принимает ограниченный набор аргументов типа в этом Международном стандарте. Если тип не является класс стандартного макета (раздел 9), результаты undefined. [...] Результат применения макроса offsetof к полю, которое является статическим элементом данных или членом функции undefined. [...]

Ответ 6

Язык не должен ничего говорить о "фактическом выполнении" из-за как правило if-if. В конце концов, без побочных эффектов, как вы могли бы определить, оценивается ли выражение? (Взгляд на сборку или установку контрольных точек не учитывается, а не часть выполнения программы, о которой говорит весь язык.)

С другой стороны, разыменование нулевого указателя имеет поведение undefined, поэтому язык ничего не говорит о том, что происходит. Вы не можете рассчитывать на то, что если вы спасете: как - если это ослабление правдоподобных ограничений на реализацию, а поведение undefined - это ослабление всех ограничений на реализацию. Поэтому нет "конфликта" между "это не имеет побочных эффектов, поэтому мы можем игнорировать его" и "это поведение undefined, поэтому носовые демоны"; они находятся на одной стороне!