Switch case: метка case не сводится к целочисленной константе

Я прекрасно знаю механизм оператора switch и почему требуется целочисленная константа. Чего я не понимаю, так это почему следующая метка case не считается целочисленной константой. Что тогда? Несуществующая переменная? Кто-нибудь может классифицировать? Компилятор C В самом деле должен быть таким тупым?

struct my_struct {
    const int my_int;
};

switch (4) {

case ((struct my_struct) { 4 }).my_int:

    printf("Hey there!\n");
    break;

}

И конечно…

error: case label does not reduce to an integer constant
  case ((struct my_struct) { 4 }).my_int:

РЕДАКТИРОВАТЬ, чтобы ответить на комментарий Юджина:

What's you real use case? If it is an integer constant, why to make it so complicated?

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

#include <stdio.h>

union my_union {
    char my_char[sizeof(int)];
    int my_int;
};

void clever_switch (const char * const my_input) {

    switch (((union my_union *) my_input)->my_int) {

    case ((union my_union) { "hi" }).my_int:

        printf("You said hi!\n");
        break;

    case ((union my_union) { "no" }).my_int:

        printf("Why not?\n");
        break;

    }

}

int main (int argc, char *argv[]) {

    char my_string[sizeof(int)] = "hi";

    clever_switch(my_string);

    return 0;

}

… Который, конечно же, не компилируется.

На моей машине ((union my_union) { "hi" }).my_int есть 26984 а ((union my_union) { "no" }).my_int есть 28526. Однако я не могу написать эти числа сам, потому что они зависят от порядка байтов машины (поэтому, очевидно, моя машина имеет обратный порядок байтов). Но компилятор знает о последнем и точно знает во время компиляции, каким будет число ((union my_union) { "no" }).my_int.

Раздражает то, что я могу уже делаю это, но только с использованием очень неясного (и чуть менее эффективного) синтаксиса. Следующий пример компилируется просто отлично:

#include <stdio.h>

void clever_switch (const char * const my_input) {

    #define TWO_LETTERS_UINT(FIRST_LETTER, SECOND_LETTER) ((unsigned int) ((FIRST_LETTER) << 8) | (SECOND_LETTER))

    switch (TWO_LETTERS_UINT(my_input[0], my_input[1])) {

    case TWO_LETTERS_UINT('h', 'i'):

        printf("You said hi!\n");
        break;

    case TWO_LETTERS_UINT('n', 'o'):

        printf("Why not?\n");
        break;

    }

    #undef TWO_LETTERS_UINT

}

int main (int argc, char *argv[]) {

    clever_switch("hi"); /* "You said hi!" */
    clever_switch("no"); /* "Why not?" */

    return 0;

}

Итак, остается вопрос: должен ли компилятор C (или стандарт C в данном случае) В самом деле быть таким тупым?

Каков ваш вариант использования настоящий? Если это целочисленная константа, зачем делать ее такой сложной?

Eugene Sh. 08.04.2019 21:10

По большей части стандарт C не заставляет компиляторы работать с объектами во время компиляции. Любая оценка времени компиляции просто требует использования простых значений. В отличие от простых значений объект по определению представляет собой область памяти, в которой байты представляют собой значение. C разработан, или, по крайней мере, изначально был разработан, чтобы быть портативным и маленьким. Одной из особенностей является кросс-компиляция: компилятор может быть разработан для работы на одной архитектуре и для компиляции программ для выполнения на другой архитектуре. В стандарте C есть различные положения для этого, такие как отдельные наборы символов…

Eric Postpischil 08.04.2019 21:34

… для компиляции и выполнения. Поэтому учтите, что компилятор C, который должен позволять вам помещать элементы в структуру, а затем извлекать их, должен эмулировать, как они будут помещаться в структуру в целевой системе, а не в исходной системе. Это может показаться простым, если просто поставить и убрать int. Но выражения C могут быть намного сложнее. Вы можете вставить int, а затем убрать один байт, используя преобразование в char *. Мы не собираемся требовать, чтобы компиляторы C подражали этому. Конечно, он может подтасовывать вещи, чтобы заставить его работать для простых int операций, таких как ваши, но стандарт C…

Eric Postpischil 08.04.2019 21:36

… были бы некоторые проблемы с объяснением того, какие именно вещи должны поддерживаться таким образом. Такого проще не поддерживать. И этого достаточно — C был чрезвычайно успешным и без этой возможности. И даже если вы не возитесь с байтами, подумайте, что произойдет, если вы определите огромный массив, инициализированный некоторыми значениями, и вытащите одно из них, чтобы использовать его как «константу». Теперь вы заставляете компилятор манипулировать большими объемами данных во время компиляции.

Eric Postpischil 08.04.2019 21:37

@ЕвгенийШ. Я отредактировал свой вопрос, чтобы ответить на ваш комментарий.

madmurphy 08.04.2019 21:43
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
6
1 141
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Хотя выражение ((struct my_struct) { 4 }).my_int действительно оценивается во время выполнения как 4, оно не является константой. Switch-case нуждается в постоянном выражении.

Я вижу, что вы объявили my_int как const. Но это означает только то, что его нельзя изменить позже. Это не означает, что выражение ((struct my_struct) { 4 }).my_int постоянно.

Если вы используете if-statement вместо switch-case, все будет в порядке.

if (((struct my_struct) { 4 }).my_int == 4) {
    printf("Hey there!\n");
}

Я предполагаю, что ОП утверждает, что это выражение могу оценивается во время компиляции.

Eugene Sh. 08.04.2019 21:20

Я считаю, что в C99 и более поздних версиях ((struct my_struct) { 4 }).my_intделает квалифицируется как постоянное выражение в том смысле, что стандарт использует этот термин, но не как целочисленное константное выражение, что требуется для switch случаев.

zwol 08.04.2019 21:21

@zwol, я только что попробовал код OP с опцией компиляции C99. Я получаю ту же ошибку.

VHS 08.04.2019 21:28

@VHS Я всего лишь придираюсь к твоей формулировке. Код OP недействителен во всех версиях стандарта C, но не потому, что ((struct my_struct) { 4 }).my_int не является константой. Это является константное [выражение], но это не целочисленное константное выражение, который является ограниченной подкатегорией константных выражений. Метки case должны быть не просто постоянными, а целочисленными константными выражениями.

zwol 08.04.2019 21:48

Метка case в операторе switch требует целочисленное константное выражение, который определяется как:

An integer constant expression shall have integer type and shall only have operands that are integer constants, enumeration constants, character constants, sizeof expressions whose results are integer constants, _Alignof expressions, and floating constants that are the immediate operands of casts. Cast operators in an integer constant expression shall only convert arithmetic types to integer types, except as part of an operand to the sizeof or _Alignof operator.

Выражение ((struct my_struct) { 4 }).my_int не квалифицируется как целочисленное константное выражение по этому определению, даже если это целочисленное выражение, значение которого может быть определено во время компиляции.

Ваш ответ немного тавтологичен. Это все равно, что сказать: «Это невозможно сделать, потому что так говорит стандарт, даже если теоретически это мог можно сделать». Мой вопрос, однако, был: «Это действительно необходимо, что это невозможно сделать?»…

madmurphy 08.04.2019 22:24

Вы спросили, почему это не считается целочисленной константой (выражением), на что я ответил. Если вместо этого вы спросите: «Почему определение целочисленного константного выражения в стандарте C не может быть расширено для включения этого случая», то мой ответ будет таким: может, но это усложнит реализацию стандарта и компилятора, и, предположительно, это ситуация не считается достаточно важной, чтобы оправдать добавленную сложность.

Tom Karzes 08.04.2019 22:32

Хорошо, справедливо!

madmurphy 08.04.2019 22:34

Это наименьший общий знаменатель.

Стандарт C говорит, что ((struct my_struct) { 4 }).my_int не удовлетворяет ограничениям, наложенным на метки case (а именно, что они являются целочисленными константными выражениями), поэтому совместимые компиляторы C не обязаны быть достаточно умными, чтобы иметь возможность оптимизировать его.

Стандарт не запрещает компилятору запретить оптимизировать его. Действительно, оптимизация — это то, что делает clang.

Ваша программа:

#include <stdio.h>
struct my_struct {
    const int my_int;
};

int main()
{
    switch (4) {

        case ((struct my_struct) { 4 }).my_int:

            printf("Hey there!\n");
            break;

    }
}

просто работает на clang, хотя вы получите предупреждение, если скомпилируете его с помощью -pedantic.

В других случаях, например, при различении VLA и обычных массивов, различие между целочисленными константными выражениями и другими целочисленными выражениями также влияет на другие конструкции, такие как переходы на основе switch или goto, которые становятся запрещенными, если они переходят в область действия VLA. Опять же, компилятор может свернуть его и разрешить такие переходы, если сделана хотя бы одна диагностика (clang предупреждает о свертывании, а не о переходе).

Если вы используете эти конструкции и ваш компилятор вас не остановит, ваша программа не будет переносимой.

Наконец, константность целых чисел во время компиляции также может влиять на типы в определенном случае.

Ядро Linux, как мне кажется, использует что-то похожее на

#define IS_CEXPR(X) _Generic((1? (void *) ((!!(X))*0ll) : (int *) 0), int*: 1, void*: 0)

для обнаружения целочисленных константных выражений (что, как я слышал, было частью миссии по отсеиванию VLA).

Это основано на стандартном правиле C, согласно которому целочисленное константное выражение, равное 0, приведенное к (void*), является константой нулевого указателя, тогда как регулярное целочисленное выражение, приведенное к (void*), является просто пустым указателем, даже если значение выражения известно. быть равным 0. Затем правила определения типа тернара различают выражения (void*) и константу нулевого указателя, в результате чего (1? (void *) ((!!(X))*0ll) : (int *) 0) типизируется int *, если X является целочисленным константным выражением, и void * в противном случае.

Большинство компиляторов, вероятно, не позволят вам так легко обойти нарушения системы типов (особенно внутри _Generic).

@madmurphy Не за что. zwol проливает больше света на происхождение. Я написал больше о том, как целочисленные константные выражения взаимодействуют с языком в его нынешнем виде. Довольно интересно, как первоначально полупроизвольное решение о том, что нужно или не нужно оценивать во время компиляции, стало концепцией, настолько вплетенной в язык, что ни один из основных современных компиляторов даже не реализует ее полностью правильно: twitter.com/pskocik/status/1076768533869146112 . Я думаю, вы могли бы сделать хорошее замечание о необходимости переосмысления.

PSkocik 08.04.2019 23:02
Ответ принят как подходящий

Что касается вопроса Зачем, стандарт C не позволяет компилятору принимать

case ((struct my_struct) { 4 }).my_int:

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

Но мы можем сказать следующее:

  1. Первоначальный стандарт C 1989 г. намеренно упустил любое количество функций, которые были реализованы в мог, но только за счет значительных затрат на сложность реализации, требования к памяти во время компиляции и т. д. Например, исходное обоснование различия между «константным выражением» и « «Константное выражение целое число» в стандарте заключалось в том, что компилятору никогда не нужно выполнять арифметические операции с плавающей запятой во время компиляции.

    Функция, о которой вы просите, примерно так же сложна для реализации, как

    static const int CONSTANT = 123;
    ...
    switch (x) { case CONSTANT: ... }
    

    который также не требуется для работы в C (хотя он есть в C++).

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

Это лучший ответ, который я могу вам дать.

Это похоже на наиболее вероятный ответ, спасибо. Это все больше и больше убеждает меня в том, что язык C настолько прекрасен, что его заслуживает заново изобретать с нуля.

madmurphy 08.04.2019 22:51

Хорошо. Почему это невозможно, кажется, было подробно объяснено... ...Тогда я просто оставлю это здесь.

case ((int)((struct my_struct) { 4 }).my_int):

лязг 9, архлинукс, x86_64

Другие вопросы по теме