Closed kov-serg closed 1 year ago
Предлагаю тем кто поставил унылых смайликов оценить разницу https://godbolt.org/z/b9hME36jf https://godbolt.org/z/eG9b8sYad и предложить варианты получше
Как один из поставивших "унылый смайлик" отвечу: я не смог придумать пример, в котором это принесёт пользу. Какая смысловая нагрузка у next_case:
? Мы не будем знать реальное значение в case, только то, что оно на 1 больше предыдущего - какой в этом смысл? Если для понимания нужно вручную вычислять это значение, то это усложняет чтение кода. Допустим, я смотрю на блок кода рядом с next_case и должен размышлять следующим образом: "он будет вызван, если значение в switch окажется на единицу больше, чем то, для которого выполнится кусок кода, расположенный на строчке выше" - так? Странно.
Более того, на практике редко используется switch по числам, чаще по enum'ам - в таком случае идея кажется ещё более абсурдной.
Приведенный пример - это какой-то невероятно частный случай, да ещё и багоопасный. Что за магическая константа N? Если я напишу больше next_case, чем N, как отлавливать ошибку?
Не убедили, в общем.
Поясню. Есть подход с коротинами. А можно разделить задачу на короткие блоки вручную. Из плюсов вы можете без особых сложностей сериализовать подобное состояние и потом загрузить его и продолжить выполнени. Так вот единственны подобный механизм можно получить с помошью 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: закрыть_холодильник();
}
Какие альтернативы есть в языке?
Альтернативу вы и сами упомянули - корутины. С их помощью это реализуется.
Как с их помошью выполнить сохранение в текущего состояния и последующего его востановления, и продолжения исполнения с места сохранения (например на другом компьютере)?
Также, как без них, это не зависит от графа выполнения кода.
Вы предлагаете добавить в язык новую синтаксическую конструкцию, для которой привели только один очень специфический пример использования, который может быть реализован через существующие возможности.
Да я предлагаю, небольшое расширение в switch которое позволит нарезать последовательный код на части. Более того подобное нововведение не является чем-то крайне сложным в реализации. "реализован через существующие возможности" - так я могу любые конструкции реализовать на любом языке. Но это не всегда удобно. А тут же при минимальных изменениях получаем удобный инструмент. Проблема в том что существующие возможности значительно уступают подобному подходу. Посмотрите то что компилируют разные компиляторы для подобных костылей. https://godbolt.org/z/b9hME36jf https://godbolt.org/z/eG9b8sYad
Более того я бы еще добавил в switch конструкцию вида:
switch(some_enum) {
case E1: { } break;
no default:
};
Где no default означало бы что в switch перечислены все допустимые значения, в обратном случае компилятор бы сообщал чего забыли и останавливался.
Синтаксис языка не меняют без весомых аргументов (один специфический кейс использования - это слабый аргумент). Даже если "реализовать" несложно, подобные изменения повлекут необходимость доработки не только компиляторов, но и IDE, статических анализаторов, и прочих утилит. Более того, нужно будет обучить всех тому, что в языке появился новый элемент. Если это будет ключевым словом, понадобится ещё и убедиться, что нет конфликтов с существующим кодом. Подчеркну, что это всё ради одного частного случая.
Всё вышеописанное - моё мнение, но комитет ещё строже.
По поводу no default: подумайте про следующую ситуацию:
enum class E { A = 0 };
switch (static_cast<E>(42)) {
case E::A: { break; }
no default:
}
По поводу 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-ах. А в случае костылей получаем менее удобный и код с излишними наворотами и оверхедом.
Есть огромная разница между изменениями синтаксиса языка и:
std::launder
, который является библиотечной фичей (не нужен большинству пользователей, но нужен, чтобы заткнуть UB в реализациях стандартных библиотек) с небольшой поддержкой от компилятора.Так что ваши сравнения абсолютно некорректны
Какие альтернативы есть в языке?
массив указателей на функции.
no default: / попадаем в эту ветку /
std::unreachable, unreachable(), assume(false)
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;
};
"Да как угодно, хоть так:" А востанавливать как?
[kelbon] массив указателей на функции.
И как именно это должно выглядеть? В каком месте это лучше? https://godbolt.org/z/enx61P9ez
Тут подумал что вместо слова next_case можно использовать break:
switch(it) { default: { /* if none */ }
break: { /*case 0*/ }
break: { /*case 1*/ }
break: { /*case 2*/ }
};
по моему в таком виде вообще будет идеально.
В каком месте это лучше?https://godbolt.org/z/enx61P9ez
Это удовлетворяет вашему же требованию:
Так в случае вставки, удаления или перестановки последовательности шагов не надо будет вручную менять индексы.
… без изменения стандарта и поломки существующего кода (т.к. next_case
должно стать ключевым словом).
В варианте 2 надо постоянно дублировать имя класса. В случае ссылки не на челен класса сломается. В варианте 3 оверхед. Причем некоторые компиляторы могут нагенерить много мусора. Не единообразна последняя запись.
Можно без "next_case" а с спец меткой "break:" - это не поломает ничего и расширит функционал. При наличии двух одинаковых меток, старый компилятор выругается, иде будут раскрашивать как и раньше. Сплошные плюсы.
Вот еще пример использования 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;
}
Еще пример. С функция возможностью возобновления исполнения. Т.е. возможна сереализацией состояния и продолжением исполения после десереализации. Коротинах есть всё кроме простоты и возможности возобновить исполение с сохраненной точки. В случае последовтельных 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
Всё. Нафиг этот ваш С++ и попытки его улучшить. Всё решается в рамках обычного 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;
}
Добавить в switch возможность авто увеличения case-ов как у enum-ов
Примеры:
Что бы не писать индексы явно:
Так в случае вставки, удаления или перестановки последовательности шагов не надо будет вручную менять индексы. Плюс реализация тривиальна.