cpp-ru / ideas

Идеи по улучшению языка C++ для обсуждения
https://cpp-ru.github.io/proposals
Creative Commons Zero v1.0 Universal
89 stars 0 forks source link

Добавить в switch последовательных case-ов #525

Closed kov-serg closed 1 year ago

kov-serg commented 1 year ago

Добавить в switch возможность авто увеличения case-ов как у enum-ов

Примеры:

int A::step() {
  switch(this->state++) {
    default: return 0;
    next_case: fn1(); break;
    next_case: fn2(); break;
    next_case: fn3(); break;
    // ...
    next_case: fnN(); break;
  }
  return 1;
}

Что бы не писать индексы явно:

int A::step() {
  switch(this->state++) {
    default: return 0;
    case 0: fn1(); break;
    case 1: fn2(); break;
    case 2: fn3(); break;
    // ...
    case N: fnN(); break;
  }
  return 1;
}

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

kov-serg commented 1 year ago

Предлагаю тем кто поставил унылых смайликов оценить разницу https://godbolt.org/z/b9hME36jf https://godbolt.org/z/eG9b8sYad и предложить варианты получше

Smertig commented 1 year ago

Как один из поставивших "унылый смайлик" отвечу: я не смог придумать пример, в котором это принесёт пользу. Какая смысловая нагрузка у next_case:? Мы не будем знать реальное значение в case, только то, что оно на 1 больше предыдущего - какой в этом смысл? Если для понимания нужно вручную вычислять это значение, то это усложняет чтение кода. Допустим, я смотрю на блок кода рядом с next_case и должен размышлять следующим образом: "он будет вызван, если значение в switch окажется на единицу больше, чем то, для которого выполнится кусок кода, расположенный на строчке выше" - так? Странно.

Более того, на практике редко используется switch по числам, чаще по enum'ам - в таком случае идея кажется ещё более абсурдной.

Приведенный пример - это какой-то невероятно частный случай, да ещё и багоопасный. Что за магическая константа N? Если я напишу больше next_case, чем N, как отлавливать ошибку?

Не убедили, в общем.

kov-serg commented 1 year ago

Поясню. Есть подход с коротинами. А можно разделить задачу на короткие блоки вручную. Из плюсов вы можете без особых сложностей сериализовать подобное состояние и потом загрузить его и продолжить выполнени. Так вот единственны подобный механизм можно получить с помошью switch остальные варианты более накладные. Так например в микроконтроллерах и всяких ардуинах да и в некоторых играх, применяется подход loop-ом который вызаваетья или постоянно или с определенным шагом по времени (например 1000 раз в сек).

void setup() {
  ...
}
void loop() {
  ...
}

Так вот подобная конструкция поволит нарезать последовательность кода на фрагменты:

switch(state) {
  next_case: открыть_холодильник(); break;
  next_case: достать_жирафа(); break;
  next_case: засунуть_бегимота(); break;
  next_case: закрыть_холодильник();
  default: done=true;
}

Чем плохо? Возможно даже более радикальный синтаксис:

switch(state) { default: done=true;
  break: открыть_холодильник();
  break: достать_жирафа();
  break: засунуть_бегимота(); 
  break: закрыть_холодильник();
}

Какие альтернативы есть в языке?

Smertig commented 1 year ago

Альтернативу вы и сами упомянули - корутины. С их помощью это реализуется.

kov-serg commented 1 year ago

Как с их помошью выполнить сохранение в текущего состояния и последующего его востановления, и продолжения исполнения с места сохранения (например на другом компьютере)?

Smertig commented 1 year ago

Также, как без них, это не зависит от графа выполнения кода.

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

kov-serg commented 1 year ago

Да я предлагаю, небольшое расширение в switch которое позволит нарезать последовательный код на части. Более того подобное нововведение не является чем-то крайне сложным в реализации. "реализован через существующие возможности" - так я могу любые конструкции реализовать на любом языке. Но это не всегда удобно. А тут же при минимальных изменениях получаем удобный инструмент. Проблема в том что существующие возможности значительно уступают подобному подходу. Посмотрите то что компилируют разные компиляторы для подобных костылей. https://godbolt.org/z/b9hME36jf https://godbolt.org/z/eG9b8sYad

Более того я бы еще добавил в switch конструкцию вида:

switch(some_enum) {
 case E1: { } break;
 no default: 
};

Где no default означало бы что в switch перечислены все допустимые значения, в обратном случае компилятор бы сообщал чего забыли и останавливался.

Smertig commented 1 year ago

Синтаксис языка не меняют без весомых аргументов (один специфический кейс использования - это слабый аргумент). Даже если "реализовать" несложно, подобные изменения повлекут необходимость доработки не только компиляторов, но и IDE, статических анализаторов, и прочих утилит. Более того, нужно будет обучить всех тому, что в языке появился новый элемент. Если это будет ключевым словом, понадобится ещё и убедиться, что нет конфликтов с существующим кодом. Подчеркну, что это всё ради одного частного случая.

Всё вышеописанное - моё мнение, но комитет ещё строже.

По поводу no default: подумайте про следующую ситуацию:

enum class E { A = 0 };

switch (static_cast<E>(42)) {
  case E::A: { break; }
  no default:
}
kov-serg commented 1 year ago

По поводу no default не вижу никаках противоречий:

switch (static_cast<E>(42)) {
  case E::A: { } break; 
  no default: /* попадаем в эту ветку */
}

Подобная конструкция нужна только для того что бы найти все места где могли упустить case при добавлении в enum нового значения. "Синтаксис языка не меняют без весомых аргументов" -- то есть для наркоманских конструкций https://en.cppreference.com/w/cpp/utility/launder которые выворачивают наружу внутренности компилятора были аргументы. А тут вы считаете что аргументов не достаточно.

"ради одного частного случая" -- коротины же добавили (кое-как) это тоже всего лишь один частный случай. Они и раньше в обычном C легко реализовывались средствами языка с помощью setjmp/longjmp. Видимо просто вы не стой стороны смотрите на подобный частный случай. Еще раз повторю что и сейчас можно использовать обычный switch просто в случае добавления, удаления или перестановки частков приходится постоянно править цифры в case-ах. А в случае костылей получаем менее удобный и код с излишними наворотами и оверхедом.

Smertig commented 1 year ago

Есть огромная разница между изменениями синтаксиса языка и:

  1. std::launder, который является библиотечной фичей (не нужен большинству пользователей, но нужен, чтобы заткнуть UB в реализациях стандартных библиотек) с небольшой поддержкой от компилятора.
  2. Корутинами, которые никак нельзя назвать частным случаем - они упрощают громадное количество как библиотечного асинхронного кода, так и пользовательского, не говоря уже про недавно принятые генераторы, например.

Так что ваши сравнения абсолютно некорректны

kelbon commented 1 year ago

Какие альтернативы есть в языке?

массив указателей на функции.

kelbon commented 1 year ago

no default: / попадаем в эту ветку /

std::unreachable, unreachable(), assume(false)

kin4stat commented 1 year ago

std::unreachable, unreachable(), assume(false)

Ага, и в примере выше получаем отстрел ноги :)

Да я предлагаю, небольшое расширение в switch которое позволит нарезать последовательный код на части. Более того подобное нововведение не является чем-то крайне сложным в реализации.

Для вас в язык специально добавили stackless корутины, зачем делать еще один сомнительный велосипед для случая один на миллион? Свитч по чиселкам самый непопулярный юзкейс, к чему это вообще?

Подобная конструкция нужна только для того что бы найти все места где могли упустить case при добавлении в enum нового значения.

Так зачем этим нагружать компилятор? Его задача компилировать код, а не ошибки в нем искать. Если очень хочется - всегда есть инструменты реализованные поверх куска компиляторов.

clang-tidy myfile.cpp -checks=hiccp-multiway-path-covered

И не бойтесь - от вас ни один случай не ускользнет, потому что оно простое как валенок

постоянно править цифры в case-ах

Если у вас там цифры вместо enum'ов и у вас при большом желании отлавливать ошибки не подключен специализированный инструмент для этой цели - вы ССЗБ

Как с их помошью выполнить сохранение в текущего состояния и последующего его востановления, и продолжения исполнения с места сохранения (например на другом компьютере)?

Да как угодно, хоть так:

struct save_state {};

struct promise_type {
    /* ... */
    auto await_transform() {
        /* ... */
        ++current_state;
        /* ... */
    }

    auto await_transform(save_state) {
        struct {
            /* ... */
            void await_suspend(std::coroutine_handle<promise_type> h) const {
                std::ostream state{ "state.txt" };
                state << state;
            }
            /* ... */

            int& state;
        } save_state_object{ current_state };
        return save_state_object;
    }
    /* ... */

    int& current_state;
};
kov-serg commented 1 year ago

"Да как угодно, хоть так:" А востанавливать как?

kov-serg commented 1 year ago

[kelbon] массив указателей на функции.

И как именно это должно выглядеть? В каком месте это лучше? https://godbolt.org/z/enx61P9ez

Тут подумал что вместо слова next_case можно использовать break:

switch(it) { default: { /* if none */ }
break: { /*case 0*/ }
break: { /*case 1*/ }
break: { /*case 2*/ }
};

по моему в таком виде вообще будет идеально.

pavelkryukov commented 1 year ago

В каком месте это лучше?https://godbolt.org/z/enx61P9ez

Это удовлетворяет вашему же требованию:

Так в случае вставки, удаления или перестановки последовательности шагов не надо будет вручную менять индексы.

… без изменения стандарта и поломки существующего кода (т.к. next_case должно стать ключевым словом).

kov-serg commented 1 year ago

В варианте 2 надо постоянно дублировать имя класса. В случае ссылки не на челен класса сломается. В варианте 3 оверхед. Причем некоторые компиляторы могут нагенерить много мусора. Не единообразна последняя запись.

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

kov-serg commented 1 year ago

Вот еще пример использования https://godbolt.org/z/b9vaMGxxE

#include <stdio.h>

struct Loop {
    int timer, state, exit_code;
    Loop() { setup(); }
    Loop* setup() { timer=0; state=0; exit_code=-1; return this; }
    bool timeout(int limit) const { return timer>limit; }
    void exit(int code=0) { exit_code=code; }
    void next() { state++; timer=0; }
    //...
};

struct Actor {
    enum { SOME_COMMAND=47, ACK=5, NACK=7, INVALID=255 } received;
    bool recv() { received=ACK; printf(" recv"); return true; }
    void send(int cmd) { printf(" send"); }
};

struct Walker {
    Loop loop[1]; Actor *actor;
    Loop* setup(Actor *actor) {
        this->actor=actor;
        return loop->setup();
    }
#if 0
    void step() {
        switch(loop->state) { default: loop->exit();
        break;case 0:
            if (loop->timeout(500)) loop->next();
        break;case 1:
            actor->send(Actor::SOME_COMMAND); loop->next();
        break;case 2: 
            if (loop->timeout(500)) loop->exit(1);
            if (actor->recv()) loop->next();
        break;case 3: 
            if (actor->received==Actor::ACK) loop->next();
            else if (actor->received==Actor::NACK) loop->exit(2);
            else loop->exit(3);
        break;case 4:
            if (loop->timeout(500)) loop->next();
        }
    }
#else
    void step() {
        switch(loop->state) { default: loop->exit();
        break:
            if (loop->timeout(500)) loop->next();
        break:
            actor->send(Actor::SOME_COMMAND); loop->next();
        break:
            if (loop->timeout(500)) loop->exit(1);
            if (actor->recv()) loop->next();
        break:
            if (actor->received==Actor::ACK) loop->next();
            else if (actor->received==Actor::NACK) loop->exit(2);
            else loop->exit(3);
        break:
            if (loop->timeout(500)) loop->next();
        }
    }
#endif
};

int main(int argc, char const *argv[]) {
    Actor actor[1];
    Walker walker[1];
    Loop *loop=walker->setup(actor);
    for(int i=0;i<20;i++) {
        printf("i=%2d t=%3d state=%d",i,loop->timer,loop->state);
        loop->timer+=100;
        walker->step();
        printf("\n");
        if (loop->exit_code>=0) break;
    }
    printf("exit_code=%d\n",loop->exit_code);
    return 0;
}
kov-serg commented 1 year ago

Еще пример. С функция возможностью возобновления исполнения. Т.е. возможна сереализацией состояния и продолжением исполения после десереализации. Коротинах есть всё кроме простоты и возможности возобновить исполение с сохраненной точки. В случае последовтельных case-ов можно было бы обойтись без явных номеров точек. А так, в случае изменений, их придётся постоянно перенумеровывать.

#include <stdio.h>

#define CHECK_POINTS_BEGIN() switch(check_point) { default:
#define CHECK_POINT(n)       case n: check_point=n;
#define CHECK_POINTS_END()   check_point=-1; }

struct Example {
    int check_point,i;
    Example() { check_point=0; }
    void loop(int it=-1) {
        CHECK_POINTS_BEGIN()
        CHECK_POINT(0) if (!it--) return;
        printf("p1\n");
        CHECK_POINT(1) if (!it--) return;
        printf("p2\n");
        for(i=1;i<=4;i++) {
            CHECK_POINT(2) if (!it--) return;
            printf("p3.%d\n",i); 
        }
        CHECK_POINT(3) if (!it--) return;
        printf("p4\n"); 
        CHECK_POINT(4) if (!it--) return;
        printf("p5\n"); 
        CHECK_POINTS_END()
    }
};

int main(int argc, char const *argv[]) {
    Example e;
    e.loop(3);
    printf("--\n");
    e.loop(2);
    printf("--\n");
    e.loop();
    return 0;
}
p1
p2
p3.1
--
p3.2
p3.3
--
p3.4
p4
p5
kov-serg commented 1 year ago

Всё. Нафиг этот ваш С++ и попытки его улучшить. Всё решается в рамках обычного C.

// track-function.c

#define TRACK_START switch(st->line) { default: TRACK_POINT
#define TRACK_POINT case __LINE__: st->line=__LINE__; if (!st->it--) { st->it=1; return 1; }
#define TRACK_END   st->line=-1; return 0; } st->it=1; return 1;

typedef struct fn_state_s {
    int line, it, i;
} fn_state;

void fn_reset(fn_state *st) {
    st->line=-1; st->it=1;
}
int fn(fn_state *st) {
    TRACK_START
        printf("p1\n");
        TRACK_POINT
        printf("p2\n");
        TRACK_POINT
        printf("p3\n");
        for(st->i=0;st->i<4;st->i++) {
            TRACK_POINT
            printf("p4.%d\n",st->i);
        }
        TRACK_POINT
        printf("p5\n");
        TRACK_POINT
        printf("p6\n");
    TRACK_END
}

int main(int argc, char const *argv[]) {
    fn_state s[1];
    fn_reset(s);

    s->it=3; fn(s);
    printf("--\n");
    s->it=2; fn(s);
    printf("--\n");
    do { printf("\t"); } while(fn(s));
    return 0;
}