ideas icon indicating copy to clipboard operation
ideas copied to clipboard

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

Open kov-serg opened this issue 3 years ago • 18 comments

Добавить в 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 avatar Jul 27 '22 10:07 kov-serg

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

kov-serg avatar Aug 04 '22 08:08 kov-serg

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

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

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

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

Smertig avatar Aug 04 '22 21:08 Smertig

Поясню. Есть подход с коротинами. А можно разделить задачу на короткие блоки вручную. Из плюсов вы можете без особых сложностей сериализовать подобное состояние и потом загрузить его и продолжить выполнени. Так вот единственны подобный механизм можно получить с помошью 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: закрыть_холодильник();
}

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

kov-serg avatar Aug 05 '22 16:08 kov-serg

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

Smertig avatar Aug 05 '22 17:08 Smertig

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

kov-serg avatar Aug 05 '22 17:08 kov-serg

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

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

Smertig avatar Aug 06 '22 08:08 Smertig

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

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

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

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

kov-serg avatar Aug 06 '22 10:08 kov-serg

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

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

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

enum class E { A = 0 };

switch (static_cast<E>(42)) {
  case E::A: { break; }
  no default:
}

Smertig avatar Aug 06 '22 11:08 Smertig

По поводу 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-ах. А в случае костылей получаем менее удобный и код с излишними наворотами и оверхедом.

kov-serg avatar Aug 06 '22 13:08 kov-serg

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

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

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

Smertig avatar Aug 06 '22 13:08 Smertig

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

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

kelbon avatar Aug 06 '22 14:08 kelbon

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

std::unreachable, __unreachable(), __assume(false)

kelbon avatar Aug 06 '22 14:08 kelbon

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;
};

kin4stat avatar Aug 06 '22 16:08 kin4stat

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

kov-serg avatar Aug 06 '22 20:08 kov-serg

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

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

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

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

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

kov-serg avatar Aug 07 '22 14:08 kov-serg

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

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

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

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

pavelkryukov avatar Aug 09 '22 21:08 pavelkryukov

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

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

kov-serg avatar Aug 10 '22 08:08 kov-serg

Вот еще пример использования 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 avatar Aug 11 '22 21:08 kov-serg

Еще пример. С функция возможностью возобновления исполнения. Т.е. возможна сереализацией состояния и продолжением исполения после десереализации. Коротинах есть всё кроме простоты и возможности возобновить исполение с сохраненной точки. В случае последовтельных 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 avatar Nov 03 '22 21:11 kov-serg

Всё. Нафиг этот ваш С++ и попытки его улучшить. Всё решается в рамках обычного 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;
}

kov-serg avatar Nov 04 '22 09:11 kov-serg