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

Чтобы показать эту тему, я собираюсь использовать C, но тот же макрос можно использовать и в 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 existsn», STR_MEMBER(a, value)); return 0; }

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

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

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

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

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

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

Примечания и исправления

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

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

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

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

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

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

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

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

error: initializer element is not constant

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

Поскольку вы должны задать этот вопрос, вероятно, было бы неплохо просто не полагаться на него, независимо от того, гарантированно ли выражение не будет выполняться. Будущие читатели вашего кода / ваши коллеги / будущие вы (во время отладки), вероятно, не будут знать, действительно ли это.   —  person ShinTakezou    schedule 21.09.2017

все определяется в терминах правила «как если бы», применяемого к абстрактной машине, определенной в стандарте.   —  person ShinTakezou    schedule 21.09.2017

Обратите внимание, что доступ к элементу нулевого указателя является неопределенным поведением. Это позволяет компилятору делать все, что он хочет.   —  person ShinTakezou    schedule 21.09.2017

Думаю, весь вопрос можно перефразировать так: (struct S*)NULL)->value; строка UB? Ответ: да, я считаю …   —  person ShinTakezou    schedule 21.09.2017

Почему бы не использовать sizeof((struct S *)0->X); вы знаете, что sizeof() не оценивает свой операнд, но он потерпит неудачу, если X не является членом struct S.   —  person ShinTakezou    schedule 21.09.2017

А разыменование nullptr — это UB.   —  person ShinTakezou    schedule 21.09.2017

В C ++ вы можете написать трейты, чтобы знать, существует ли A::value, см. std::experimental::is_detected.   —  person ShinTakezou    schedule 21.09.2017

@VTT: не удалось бы использовать методы перегрузки.   —  person ShinTakezou    schedule 21.09.2017

@VTT этот вопрос помечен как для C, так и для C ++.   —  person ShinTakezou    schedule 21.09.2017

@ Jarod42 и другие, уловка (была?) Распространена и основана на том факте, что не должно происходить никакого разыменования. Вроде как есть, но это не так. Даже в этом моем случае: если выражение на самом деле никогда не оценивается, UB не применяется, но является сутью вопроса, происходит ли оценка или нет.   —  person ShinTakezou    schedule 21.09.2017

@JonathanLeffler определенно третье исправление … хотя offsetof избегает шоу (struct S *)NULL, которое многих озадачивает.   —  person ShinTakezou    schedule 21.09.2017

Вы даже говорите в своем вопросе, что оператор запятой должен оценивать левый операнд, но затем задает вопрос, гарантированно ли это НЕ, вы противоречите тому, что вы уже знаете   —  person ShinTakezou    schedule 21.09.2017

Это может случиться, и вы доказали это, используя volatile. Дело в том, что компилятор может подумать, что это какая-то особая память, чтение из которой запускает какое-то неизвестное действие (например, чтение некоторых аппаратных регистров иногда имеет некоторые побочные эффекты) и должно быть выполнено.   —  person ShinTakezou    schedule 21.09.2017

@ M.M очевидно. Если вы рассудите лучше, вы поймете, что в стандарте может быть указано что-то вроде оценки, которую необходимо пропустить в следующих случаях, когда компилятор может гарантировать отсутствие побочного эффекта:… следуйте списку…. Я не особо вникал в них, как вы можете себе представить по этому вопросу, но даже в этих нескольких прочитанных строках я иногда находил сюрпризы.   —  person ShinTakezou    schedule 21.09.2017

Программа имеет неопределенное поведение. Одно из законных проявлений неопределенного поведения — отсутствие сбоев. На самом деле, здесь не о чем обсуждать.   —  person ShinTakezou    schedule 21.09.2017

Извините, другая часть вопроса, а именно, как заставить компиляцию завершиться ошибкой, если запрошенный член структуры не существует, на самом деле хорошо определена и имеет ответ. Вы можете использовать sizeof, как предлагали другие, или неиспользованную ветвь условного оператора, например. (0?(void)((type*)0)->member:(void)0). Выражение в левой ветви гарантированно не будет вычислено.   —  person ShinTakezou    schedule 21.09.2017

@ n.m. Вопрос также в моем незнании того, что должны говорить стандарты. Была вероятность, что левый операнд оператора запятой мог быть не вычислен (надеюсь, с использованием правильного слова…) при определенных условиях в соответствии со стандартом, так что UB не мог быть запущен. Оказалось, что это не так.   —  person ShinTakezou    schedule 21.09.2017

Гарантируется ли, что левый операнд в операторе запятой не будет фактически выполнен, если он не имеет побочных эффектов? — Вам даже не нужно ничего знать о C ++ (за исключением того факта, что он является полным по Тьюрингу), чтобы ответить на этот вопрос: выяснение, есть ли у левого операнда побочные эффекты, эквивалентно решению проблемы остановки. Очевидно, что стандарт не может заставить разработчиков компилятора решить проблему остановки, поэтому такой пункт не может существовать в стандарте.   —  person ShinTakezou    schedule 21.09.2017

Вопросы философа: как узнать, выполняется строка кода или нет, если у нее нет побочных эффектов? И: в вашем примере левый операнд может выдать segfault; разве это не побочный эффект?   —  person ShinTakezou    schedule 21.09.2017

На C лучше писать #define STR_MEMBER(S, X) ((struct S){.X = 0}, #X), что на 100% безопасно. Правильным решением будет, конечно, не изобретать такие ужасные макросы, а действовать в зависимости от типа. В C есть _Generic, а в C ++ есть шаблоны. Я не думаю, что этот макрос выполняет какую-либо задачу на любом языке, пахнет проблемой XY.   —  person ShinTakezou    schedule 21.09.2017

Вместо использования указателей вы можете использовать значения на месте на обоих языках, если абстрагируете часть на месте до вспомогательного макроса: #define STR_MEMBER(S,X) (sizeof(VALUE(S).X), #X) с VALUE(S), определенным как std::declval<S>() или (struct S){0}, в зависимости от состояния __cplusplus.   —  person ShinTakezou    schedule 21.09.2017

@ JörgWMittag интересно: значит, gcc решил это! — или, что есть по крайней мере один случай, для которого компиляторы могут «видеть», что единственным результатом выражения является чтение значения … которое затем отбрасывается (потому что это левый операнд запятой op), следовательно, оно может быть оптимизирован простым удалением. В этом и других случаях стандарт может предписывать запрет на выполнение операций, это вопрос принятия решения об этом.   —  person ShinTakezou    schedule 21.09.2017

@FedericoPoloni чушь. Компилятор создает код, а до этого промежуточное представление, которое может быть «проанализировано» для определения многих вещей, в том числе, если бы определенный «фрагмент» не имел бы никакого эффекта, если бы код был сгенерирован. Конечно, все физические изменения в процессоре, которые происходят при выполнении любого фрагмента кода, можно рассматривать как побочные эффекты этого кода. Но обычно я имею в виду не это, и, надеюсь, я не одинок.   —  person ShinTakezou    schedule 21.09.2017

@Lundin Я написал предложение, чтобы избежать комментариев, например, придумывать такие ужасные макросы. Можете ли вы решить проблему с _Generic и шаблонами? Позже я увижу ваш ответ, где после ритуала это UB, есть объяснение того, как вы бы использовали _Generic (в любом случае C11) и шаблоны для достижения того, что я хотел. Макрос соответствует цели, описанной в вопросе. Не знаю, как пахнут проблемы XY.   —  person ShinTakezou    schedule 21.09.2017

@ShinTakezou Я хочу сказать, что не должно быть ситуации, когда вам нужно выяснить, какие члены структуры имеют во время выполнения, поскольку члены определяются во время компиляции. Необходимость в этом предполагает непонятный дизайн для начала, отсюда проблема XY — то, что, по вашему мнению, вам нужно, не обязательно является лучшим решением.   —  person ShinTakezou    schedule 22.09.2017

С _Generic вы не будете писать макрос, чтобы увидеть, существует ли тип, но, возможно, для доступа к нему безопасным способом. Учитывая правильную структуру typedef’d typedef struct { int value; } a_t;, вы могли бы, например, написать что-то вроде #define get_value(name) _Generic((name), a_t: (name).value) и назвать это как a_t a; int something = get_value(a)   —  person ShinTakezou    schedule 22.09.2017

В противном случае составные литералы внутри макроса — лучший способ решить проблему. В некоторых ответах на Как создать типобезопасные перечисления? используются очень похожие методы.   —  person ShinTakezou    schedule 22.09.2017

@Lundin узнайте, какие члены структуры имеют во время выполнения. Неа. Я хотел создать своего рода метапрограммирование, в котором все должно выполняться во время компиляции. Более того, даже неправильный макрос работает, потому что во время компиляции gcc оптимизирует его, и не существует кода, который действительно мог бы выполнить доступ во время выполнения. (Под выполнением доступа я подразумеваю кусок ассемблерного кода, который читает из адреса памяти 0 плюс смещение члена. Если бы я видел такой код, используя -S, этого вопроса, вероятно, не существовало бы.)   —  person ShinTakezou    schedule 23.09.2017

@Lundin _Generic… Мне нужна не безопасность типов, а строка (известная во время компиляции), которая содержит буквы, которые являются символом члена структуры. Моя первоначальная мысль действительно заключалась в том, чтобы написать минимальный синтаксический анализатор для struct, способный генерировать еще struct, объединяющий другую информацию, а затем все вставить .h — все это для того, чтобы убедиться, что строки не содержат опечаток … Но потом я подумал, может ли это сделать препроцессор / компилятор во время компиляции. Реальное использование не было похоже на данный пример, но больше похоже на struct info xxx_info[] = {{STR_MEMBER{xxx,yyy}, /*…*/},/*…*/}; Все данные известны во время компиляции.   —  person ShinTakezou    schedule 23.09.2017

Источник: ledsshop.ru

Стиль жизни - Здоровье!