Чтобы показать эту тему, я собираюсь использовать 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 exists\n", 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
оператор запятой (документация C, говорит нечто очень похожее) не имеет таких гарантий.
нерелевантная информация опущена
Проще говоря,
E1
будет оцениваться, хотя компилятор может оптимизировать его с помощью правила «как если бы», если он сможет определить отсутствие побочных эффектов.Все наоборот. Стандарт гарантирует, что левый операнд оценивается (на самом деле это так, исключений нет). Результат отброшен.
Примечание. для выражений lvalue «оценка» не означает «доступ к сохраненному значению». Вместо этого это означает определить, где находится назначенная ячейка памяти. Другой код, охватывающий выражение lvalue, может затем продолжить, а может и не получить доступ к ячейке памяти. Процесс чтения из области памяти известен как «преобразование lvalue» в C или «преобразование lvalue в rvalue» в C ++.
В C ++ для выражения отброшенного значения (например, для левого операнда оператора запятой) выполняется преобразование lvalue в rvalue, только если оно равно
volatile
, а также соответствует некоторым другим критериям (подробности см. В C ++ 14 [expr] / 11 ). В C преобразование lvalue действительно происходит для выражений, результат которых не используется (C11 6.3.2.1/2).В вашем примере неясно, происходит ли преобразование lvalue. В обоих языках
X->Y
, гдеX
— указатель, определяется как(*X).Y
; в C действие применения*
к нулевому указателю уже вызывает неопределенное поведение (C11 6.5.3 / 3), а в C ++ оператор.
определен только для случая, когда левый операнд фактически обозначает объект (C ++ 14 [ expr.ref] /4.2).Если позже ничего не говорится о том, что на самом деле ничего не оценивается в перечисленных условиях … Вы подразумеваете, что нет текста, заявляющего об этом, я полагаю, вы проверили, но, может быть, было бы яснее указать, что нет таких исключения. — person ShinTakezou; 21.09.2017
@ShinTakezou Нет ничего подобного. Вы можете прочитать определение оператора запятой и увидеть, что он не говорит, что левый операнд иногда не оценивается или что-то еще — person ShinTakezou; 21.09.2017
Определение может быть длиннее нескольких строк, его можно разбить на несколько абзацев, охватывающих несколько случаев. Я полагаю, вы утверждаете, что это не относится к оператору запятой. — person ShinTakezou; 21.09.2017
Возможно, и вам потребовалось бы меньше времени, чтобы добавить к своему ответу, что в стандартах нет исключений. (Согласно предложению, данному в моем первом комментарии) — person ShinTakezou; 21.09.2017
@ShinTakezou Я думаю, что стандарт гарантирует, что левый операнд вычислен, уже ясно подразумевает, что нет никаких исключений. — person ShinTakezou; 21.09.2017
Обратите внимание, что OP объединяет оценку и получает доступ к значению. В частности, в C ++ преобразование lvalue-to-rvalue не применяется к отброшенному выражению glvalue, не имеющему типа с изменяемым типом. Тем не менее, UB в этом случае исходит от
->
, поэтому будет ли сделана последующая попытка доступа к сохраненному значению, не имеет значения. — person ShinTakezou; 21.09.2017@ T.C. Да, как ни странно, я изначально думал добавить это к своему ответу, но решил не усложнять ситуацию (возможно, неправильное решение). Спасибо за четкое изложение вопроса — person ShinTakezou; 21.09.2017
@ T.C. возможно, случай C ++ будет немного сложнее, если будет принято пустое предложение lvalue; В C ++ 14 я считаю, что нас спасает положение о том, что lvalue фактически должно обозначать хранилище (поскольку использование
*
для нулевого указателя не является явным UB) — person ShinTakezou; 21.09.2017@ T.C. по моей вине, я использовал термины без особого жаргона, возможно, просто смешивая синтаксический анализ / проверки времени компиляции с оценкой = генерацией кода, который будет запускаться и вызывать проблемы. — person ShinTakezou; 21.09.2017
@ M.M Доступ членов класса к чему-то, что не является объектом правильного типа, в настоящее время является UB по пропуску; пустые lvalues не изменят это, за исключением, возможно, явного значения UB. — person ShinTakezou; 21.09.2017
clang создаст код, который вызывает ошибку, если вы передадите ему параметр
-fsanitize=undefined
. Что должно ответить на ваш вопрос: по крайней мере, разработчики одной из основных реализаций явно считают, что код имеет неопределенное поведение. И они правы.Я бы поискал что-нибудь, что гарантированно не оценит выражение. Ваше предложение
offsetof
выполняет свою работу, но иногда может привести к отклонению кода, который в противном случае был бы принят, например, когдаX
равноa.b
. Если вы хотите, чтобы это было принято, я бы подумал об использованииsizeof
, чтобы выражение оставалось неоцененным.Я думаю, что пойду на
sizeof
. К сожалению, когда я проводил свои эмпирические проверки, у меня не было хлопка в руке.-fsanitize=undefined
тоже принимается gcc 6.3, но вроде все в порядке… clang 3.0-6.2 тоже принимает, но тот же результат, за исключением предупрежденийexpression result unused
. На самом деле я тестирую другой код, в котором макрос используется только для заполнения структуры. — person ShinTakezou; 21.09.2017Ты спрашиваешь,
Как отмечали другие, ответ — «нет». Напротив, стандарты оба безоговорочно заявляют, что левый операнд оператора запятой оценивается, и что результат отбрасывается.
Это, конечно, описание модели выполнения абстрактной машины; реализациям разрешено работать по-другому, пока наблюдаемое поведение такое же, как поведение абстрактной машины. Если действительно оценка левого выражения не дает побочных эффектов, тогда это разрешает полностью пропустить его, но ни в одном стандарте нет ничего, что бы требовало, чтобы оно было пропущено.
Что касается его исправления, у вас есть различные варианты, некоторые из которых применимы только к одному или другому из двух названных вами языков. Мне нравится ваша альтернатива
offsetof()
, но другие отмечали, что в C ++ есть типы, к которымoffsetof
не могут применяться. В C, с другой стороны, стандарт специально описывает его применение к типам структуры, но ничего не говорит о типах объединения. Его поведение для типов объединения, хотя, скорее всего, будет последовательным и естественным, поскольку технически не определено.Только в C вы можете использовать составной литерал, чтобы избежать неопределенного поведения в вашем подходе:
Это одинаково хорошо работает с типами структуры и объединения (хотя для этой версии необходимо указать полное имя типа, а не только тег). Его поведение хорошо определено, когда данный тип действительно имеет такой член. Расширение нарушает языковое ограничение — таким образом, требует выдачи диагностики — когда тип не имеет такого члена, в том числе когда он не является ни структурным типом, ни типом объединения.
Вы также можете использовать
sizeof
, как предлагает @alain, потому что, хотя выражениеsizeof
будет оцениваться, его операнд не будет оцениваться (за исключением C, когда его операнд имеет изменяемый тип, который будет не относится к вашему использованию). Я думаю, что этот вариант будет работать как на C, так и на C ++, без какого-либо неопределенного поведения:Я снова написал его так, чтобы он работал как для структур, так и для объединений.
Левый операнд оператора запятой — это выражение отброшенного значения
Есть также неоцененные операнды, которые, как следует из названия, не оцениваются.
Использование выражения отброшенного значения в вашем варианте использования является неопределенным поведением, а использование неоцененного операнда — нет.
Например, использование
sizeof
не вызовет UB, потому что он принимает неоцененный операнд.sizeof
предпочтительнееoffsetof
, потому чтоoffsetof
не может использоваться для статических членов и классов, не являющихся стандартными:offsetof
должно появиться где-нибудь, по крайней мере, в стандарте C, если я хорошо помню. Он отброшен или не оценен, какsizeof
? — person ShinTakezou; 21.09.2017Смотрел черновик C ++ N4296,
offsetof
— это макрос. На первый взгляд, я не нашел в нем ничего особенного. Ноsizeof
описывается как имеющий неоцененный операнд. — person ShinTakezou; 21.09.2017gcc определяет его как встроенный, я пробовал использовать его с
volatile
, вроде нормально (без segfault), но я не могу найти уверенности в этом поведении. — person ShinTakezou; 21.09.2017@ShinTakezou Кроме того, первый операнд
offsetof
должен быть классом стандартного макета, иначе поведение не определено.sizeof
не имеет этого ограничения. (В C ++) — person ShinTakezou; 21.09.2017хороший балл за
sizeof
противoffsetof
. Структура была POD, но мало ли, может быть, она изменится! — person ShinTakezou; 21.09.2017В языке не нужно ничего говорить о «фактическом выполнении» из-за как если бы правило. В конце концов, без побочных эффектов, как вы можете определить, оценивается ли выражение? (Просмотр сборки или установка точек останова не в счет; это не часть выполнения программы, а это все, что описывает язык.)
С другой стороны, разыменование нулевого указателя является неопределенным поведением, поэтому язык вообще ничего не говорит о том, что происходит. Вы не можете рассчитывать на то, что вас спасет: as-if — это ослабление правдоподобных ограничений реализации, а неопределенное поведение — это ослабление всех ограничений. по реализации. Следовательно, нет «конфликта» между «это не имеет побочных эффектов, поэтому мы можем игнорировать это» и «это неопределенное поведение, так что носовые демоны»; они на одной стороне!
Я бы сказал, что первый абзац был бы причиной предложить добавить обязательное «устранение» случая отсутствия побочных эффектов — исключенного значения. / Насчет 2-й части непонятно, насколько они на одной стороне… В общем, несколько дней назад я пошутил с другом о том, что контейнеры и подобные технологии меняют выражение «это работает на моей машине» на «я могу гарантировать, что он работает на всех этих машинах (например, на моей) ». Это могло бы подтолкнуть интересный сдвиг парадигмы и удалить этот раздражающий штамп о «носовых демонах» и уже упомянутый приятный «он работает на моей машине»! — person ShinTakezou; 21.09.2017
Вы не можете сделать обязательным исключение всех оценок без побочных эффектов, поскольку это неразрешимо. В таком случае, насколько же точно должна потребоваться реализация, чтобы попытаться доказать что-то устранимое? / Эти соображения совпадают, потому что они оба позволяют реализации делать то, что не является результатом простого чтения источника. (И это работает на моей машине, на самом деле это просто означает, что я еще не нашел случая, в котором произошел сбой. Контейнеры или нет, это не замена правильности.) — person ShinTakezou; 21.09.2017
У компилятора есть что-то вроде
(discard (read 0 16))
в придуманном на лету представлении части результата синтаксического анализа (плюс что-то еще)(((p*)0)->x, "x")
. Специальное правило для запятой op может рассматривать конечное число четко определенных случаев, например литералы, константные выражения и операции «только для чтения» (безvolatile
). С этими несколькими случаями можно справиться. В любом случае, нет смысла обсуждать это здесь дальше. / Например. рассмотрим этот самый случай: gcc vN.N… не выдает код для доступа к0->X
. Следовательно, он работает на моей машине с этим компилятором, таким образом… Ситуация, которая дает сбой, здесь вообще не существует. — person ShinTakezou; 21.09.2017