Чистые способы сделать несколько отмен в C
Кто-то, вероятно, скажет что-то об исключениях... но в C, каковы другие способы сделать следующее чисто/ясно и без повторения такого большого количества кода?
if (Do1()) { printf("Failed 1"); return 1; }
if (Do2()) { Undo1(); printf("Failed 2"); return 2; }
if (Do3()) { Undo2(); Undo1(); printf("Failed 3"); return 3; }
if (Do4()) { Undo3(); Undo2(); Undo1(); printf("Failed 4"); return 4; }
if (Do5()) { Undo4(); Undo3(); Undo2(); Undo1(); printf("Failed 5"); return 5; }
Etc...
Это может быть одним из случаев использования gotos. Или, может быть, несколько внутренних функций...
Ответы
Ответ 1
Да, в таких случаях довольно часто использовать goto, чтобы избежать повторения.
Пример:
int hello() {
int result;
if (Do1()) { result = 1; goto err_one; }
if (Do2()) { result = 2; goto err_two; }
if (Do3()) { result = 3; goto err_three; }
if (Do4()) { result = 4; goto err_four; }
if (Do5()) { result = 5; goto err_five; }
// Assuming you'd like to return 0 on success.
return 0;
err_five:
Undo4();
err_four:
Undo3();
err_three:
Undo2();
err_two:
Undo1();
err_one:
printf("Failed %i", result);
return result;
}
Как всегда, вы, вероятно, также хотите, чтобы ваши функции были небольшими и содержательно объединяли операции, чтобы избежать большого "кода отмены".
Ответ 2
Это может быть одним из случаев использования gotos.
Конечно, давайте попробуем это. Вот возможная реализация:
#include "stdio.h"
int main(int argc, char **argv) {
int errorCode = 0;
if (Do1()) { errorCode = 1; goto undo_0; }
if (Do2()) { errorCode = 2; goto undo_1; }
if (Do3()) { errorCode = 3; goto undo_2; }
if (Do4()) { errorCode = 4; goto undo_3; }
if (Do5()) { errorCode = 5; goto undo_4; }
undo_5: Undo5(); /* deliberate fallthrough */
undo_4: Undo4();
undo_3: Undo3();
undo_2: Undo2();
undo_1: Undo1();
undo_0: /* nothing to undo in this case */
if (errorCode != 0) {
printf("Failed %d\n", errorCode);
}
return errorCode;
}
Ответ 3
Если у вас одинаковая подпись для вашей функции, вы можете сделать что-то вроде этого:
bool Do1(void) { printf("function %s\n", __func__); return true;}
bool Do2(void) { printf("function %s\n", __func__); return true;}
bool Do3(void) { printf("function %s\n", __func__); return false;}
bool Do4(void) { printf("function %s\n", __func__); return true;}
bool Do5(void) { printf("function %s\n", __func__); return true;}
void Undo1(void) { printf("function %s\n", __func__);}
void Undo2(void) { printf("function %s\n", __func__);}
void Undo3(void) { printf("function %s\n", __func__);}
void Undo4(void) { printf("function %s\n", __func__);}
void Undo5(void) { printf("function %s\n", __func__);}
typedef struct action {
bool (*Do)(void);
void (*Undo)(void);
} action_s;
int main(void)
{
action_s actions[] = {{Do1, Undo1},
{Do2, Undo2},
{Do3, Undo3},
{Do4, Undo4},
{Do5, Undo5},
{NULL, NULL}};
for (size_t i = 0; actions[i].Do; ++i) {
if (!actions[i].Do()) {
printf("Failed %zu.\n", i + 1);
for (int j = i - 1; j >= 0; --j) {
actions[j].Undo();
}
return (i);
}
}
return (0);
}
Вы можете изменить результат возврата одной из функций Do, чтобы увидеть, как она реагирует :)
Ответ 4
Для полноты немного запутывания:
int foo(void)
{
int rc;
if (0
|| (rc = 1, do1())
|| (rc = 2, do2())
|| (rc = 3, do3())
|| (rc = 4, do4())
|| (rc = 5, do5())
|| (rc = 0)
)
{
/* More or less stolen from Chris' answer:
https://stackoverflow.com/a/53444967/694576) */
switch(rc - 1)
{
case 5: /* Not needed for this example, but left in in case we'd add do6() ... */
undo5();
case 4:
undo4();
case 3:
undo3();
case 2:
undo2();
case 1:
undo1();
default:
break;
}
}
return rc;
}
Ответ 5
Используйте goto
для управления очисткой в C.
Например, проверьте стиль кодирования ядра Linux:
Обоснование использования goto
:
- безусловные утверждения легче понять, а последующее вложение уменьшается
- ошибки, не обновляя отдельные точки выхода при внесении изменений, предотвращаются
- сохраняет работу компилятора для оптимизации избыточного кода;)
Пример:
int fun(int a)
{
int result = 0;
char *buffer;
buffer = kmalloc(SIZE, GFP_KERNEL);
if (!buffer)
return -ENOMEM;
if (condition1) {
while (loop1) {
...
}
result = 1;
goto out_free_buffer;
}
...
out_free_buffer:
kfree(buffer);
return result;
}
В вашем конкретном случае это может выглядеть так:
int f(...)
{
int ret;
if (Do1()) {
printf("Failed 1");
ret = 1;
goto undo1;
}
...
if (Do5()) {
printf("Failed 5");
ret = 5;
goto undo5;
}
// all good, return here if you need to keep the resources
// (or not, if you want them deallocated; in that case initialize 'ret')
return 0;
undo5:
Undo4();
...
undo1:
return ret;
}
Ответ 6
Вероятно, есть много способов сделать это, но одна идея состоит в том, что если вы не вызовете одну функцию, если предыдущая не будет выполнена успешно, вы можете связать вызовы функций, используя else if
это так. И используя переменную, чтобы отследить, где она терпит неудачу, вы можете также использовать оператор switch
для отката.
int ret=0;
if(Do1()) {
ret=1;
} else if(Do2()) {
ret=2;
} else if(Do3()) {
ret=3;
} else if(Do4()) {
ret=4;
} else if(Do5()) {
ret=5;
}
switch(ret) {
case 5:
Undo4();
case 4:
Undo3();
case 3:
Undo2();
case 2:
Undo1();
case 1:
printf("Failed %d\n",ret);
break;
}
return ret;
Ответ 7
Да, как объясняется другими ответами, использование goto
для обработки ошибок часто уместно в C.
Тем не менее, если возможно, вы, вероятно, должны сделать свой код очистки безопасным для вызова, даже если соответствующее действие никогда не выполнялось. Например, вместо:
void foo()
{
int result;
int* p = malloc(...);
if (p == NULL) { result = 1; goto err1; }
int* p2 = malloc(...);
if (p2 == NULL) { result = 2; goto err2; }
int* p3 = malloc(...);
if (p3 == NULL) { result = 3; goto err3; }
// Do something with p, p2, and p3.
bar(p, p2, p3);
// Maybe we don't need p3 anymore.
free(p3);
return 0;
err3:
free(p3);
err2:
free(p2);
err1:
free(p1);
return result;
}
Я бы защищал:
void foo()
{
int result = -1; // Or some generic error code for unknown errors.
int* p = NULL;
int* p2 = NULL;
int* p3 = NULL;
p = malloc(...);
if (p == NULL) { result = 1; goto exit; }
p2 = malloc(...);
if (p2 == NULL) { result = 2; goto exit; }
p3 = malloc(...);
if (p3 == NULL) { result = 3; goto exit; }
// Do something with p, p2, and p3.
bar(p, p2, p3);
// Set success *only* on the successful path.
result = 0;
exit:
// free(NULL) is a no-op, so this is safe even if p3 was never allocated.
free(p3);
if (result != 0)
{
free(p2);
free(p1);
}
return result;
}
Он немного менее эффективен, поскольку требует инициализации переменных NULL
, но он более удобен в обслуживании, поскольку вам не нужны дополнительные метки. Там меньше вещей, чтобы ошибиться при внесении изменений в код. Кроме того, если есть код очистки, который вам нужен как на пути успеха, так и на пути к неудаче, вы можете избежать дублирования кода.
Ответ 8
Обычно я подхожу к такой проблеме, вкладывая условные выражения:
int rval = 1;
if (!Do1()) {
if (!Do2()) {
if (!Do3()) {
if (!Do4()) {
if (!Do5()) {
return 0;
// or "goto succeeded", or ...;
} else {
printf("Failed 5");
rval = 5;
}
Undo4();
} else {
printf("Failed 4");
rval = 4;
}
Undo3();
} else {
printf("Failed 3");
rval = 3;
}
Undo2();
} else {
printf("Failed 2");
rval = 2;
}
Undo1();
} else {
printf("Failed 1");
rval = 1;
}
return rval;
Обычно для меня DoX()
- это какое-то приобретение ресурсов, например malloc()
, а UndoX()
- соответствующие выпуски ресурсов, которые должны выполняться только в случае сбоя. Вложенность четко показывает связь между соответствующими приобретениями и выпусками и устраняет необходимость повторения кода для операций отмены. Его также очень легко написать - вам не нужно создавать или поддерживать ярлыки, и легко разместить ресурс в нужном месте, как только вы напишете приобретение.
Этот подход иногда создает глубоко вложенный код. Это не сильно беспокоит меня, но вы можете считать это проблемой.
Ответ 9
Вот ответ, который я нашел устойчивым к ошибкам.
Да. Это использует goto
. Я твердо верю, что вы должны использовать то, что дает вам наибольшую ясность, а не просто слепо следовать советам тех, кто был до вас (goto
как конструкция может создать код для спагетти, но в этом случае любой другой метод обработки ошибок обычно заканчивается более похожим на спагетти, чем используя этот метод goto
, так что IMO его превосходит).
Некоторым людям может не понравиться форма этого кода, но я оспариваю, что когда он привык к стилю, он чище, его легче читать (когда все выстраивается в очередь, конечно) и гораздо более устойчивы к ошибкам. Если у вас правильно настроен статический/статический анализ, и вы работаете с POSIX, вам в значительной степени требуется, чтобы вы кодировали таким образом, чтобы обеспечить хорошую обработку ошибок.
static char *readbuf(char *path)
{
struct stat st;
char *s = NULL;
size_t size = 0;
int fd = -1;
if (!path) { return NULL; }
if ((stat(path, &st)) < 0) { perror(path); goto _throw; }
size = st.st_size;
if (size == 0) { printf("%s is empty!\n", path); goto _throw; }
if (!(s = calloc(size, 1))) { perror("calloc"); goto _throw; }
fd = open(path, O_RDONLY);
if (fd < -1) { perror(path); goto _throw; }
if ((read(fd, s, size)) < 0) { perror("read"); goto _throw; }
close(fd); /* There really no point checking close for errors */
return s;
_throw:
if (fd > 0) close(fd);
if (s) free(s);
return NULL;
}
Ответ 10
Этот вопрос уже перегружен ответами, но я хотел бы отметить, что некоторые кодовые базы на самом деле имеют код-обертку, чтобы иметь дело с -what в основном are- исключениями, в чистом виде. Например, MuPdf реализовал некоторые хитрости, используя longjmp, который эмулирует обработку исключений. На мой взгляд, если до этого дойдет, они уже должны просто использовать C++, но это только я.
Вы можете попробовать сделать такие обертки самостоятельно. В качестве упражнения давайте подумаем о ваших требованиях и постараемся придумать (очень) грубый дизайн, который пытается их удовлетворить:
- У нас есть набор операций, которые необходимо отменить, если последующие операции завершатся неудачно;
- Несколько операций должны быть отменены в обратном порядке, в котором они были выполнены;
- Операция, которую не удалось не должно быть отменено. В конце концов, это не удалось;
- Операции, которые никогда не были достигнуты, также не должны быть отменены, поскольку они никогда не выполнялись.
- В идеале позвольте программисту быть явным: он знает, какие операции нужно отменить, и когда они это делают.
Я придумал несколько макросов для решения этой проблемы:
#include <stdio.h>
// Define some variables to keep track of when an error happened, and how many operations should be undone.
// Names are "mangled" by prefixing them with try_. You probably should come up with a better mangling scheme than this.
#define BEGIN_TRY int try_done = 0, try_error = 0, try_count = 0
// Here how this works:
// - First, count the expression as an operation that may need to be undone;
// - If no error occured yet, do the operation;
// - If it succeeds, count it as a "done" operation;
// - If it fails, signal the error
#define TRY(expression) try_count++; if(!try_error && !(expression)) try_done++; else try_error = 1
// Here we take advantage of the fact that the operations behave like a queue.
// This means that, no matter what, operations need to be undone in the same
// order everytime, and if an operation needs to be undone when there
// are N operations, it also needs to be undone when there are N+1 operations.
// So, we don't really need to maintain the queue, if the programmer puts the operations in the correct order already. We just
// need to know how many operations to undo, and how much total operations are there (because we need to start at the end)
#define ON_ERROR(expression) do { if(try_error && try_done >= try_count--) {try_done--; (expression);} } while(0)
// To simplify the test, the "jobs" that we will try to do just pass or fail depending on the argument passed.
int job(int result) {return result;}
void undo(int i) {printf("Undone %d.\n", i);}
#define PASS 0
#define FAIL 1
// Let test this
int main() {
BEGIN_TRY;
// try toying with the order (and quantity) of these.
// just remember that for each "TRY" there must be one "ON_ERROR"!
TRY(job(PASS));
TRY(job(PASS));
TRY(job(FAIL));
TRY(job(PASS));
// Because only the first two operations succeeded, we should only see the effects of undo(2) and undo(1).
ON_ERROR(undo(4));
ON_ERROR(undo(3));
ON_ERROR(undo(2));
ON_ERROR(undo(1));
}
Я не эксперт по Си, поэтому, возможно, в этом есть некоторые ошибки (писать безопасные макросы сложно), но моя точка зрения такова: если вы подумаете о своих требованиях подробно, все, что вам нужно сделать, - это найти решение, которое удовлетворяет их всех. Можно сделать еще одно замечание: очень похоже на goto
, многие люди видят макросы как зло. Не будьте одним из них: если макрос сделает ваш код более понятным, более легким для чтения, тогда, безусловно, используйте его.
Ответ 11
Если функции возвращают какой-либо указатель или дескриптор состояния (как это делает большинство функций выделения и инициализации), вы можете совершенно просто сделать это без goto
, передав начальные значения переменным. Тогда у вас может быть одна функция освобождения, которая может обрабатывать случай, когда была выделена только часть ресурсов.
Например:
void *mymemoryblock = NULL;
FILE *myfile = NULL;
int mysocket = -1;
bool allocate_everything()
{
mymemoryblock = malloc(1000);
if (!mymemoryblock)
{
return false;
}
myfile = fopen("/file", "r");
if (!myfile)
{
return false;
}
mysocket = socket(AF_INET, SOCK_STREAM, 0);
if (mysocket < 0)
{
return false;
}
return true;
}
void deallocate_everything()
{
if (mysocket >= 0)
{
close(mysocket);
mysocket = -1;
}
if (myfile)
{
fclose(myfile);
myfile = NULL;
}
if (mymemoryblock)
{
free(mymemoryblock);
mymemoryblock = NULL;
}
}
А потом просто сделайте:
if (allocate_everything())
{
do_the_deed();
}
deallocate_everything();
Ответ 12
TL; DR:
Я считаю, что это должно быть написано как:
int main (void)
{
int result = do_func();
printf("Failed %d\n", result);
}
Детальное объяснение:
Если ничего нельзя предположить о типах функций, мы не можем легко использовать массив указателей на функции, которые в противном случае были бы правильным ответом.
Предполагая, что все типы функций несовместимы, мы должны были бы обернуть оригинальный неясный дизайн, содержащий все эти несовместимые функции, во что-то еще.
Мы должны сделать что-то читаемое, удобное в обслуживании, быстрое. Мы должны избегать жесткой связи, чтобы поведение отмены "Do_x" не зависело от поведения отмены "Do_y".
int main (void)
{
int result = do_func();
printf("Failed %d\n", result);
}
Где do_func
- это функция, выполняющая все вызовы, требуемые алгоритмом, а printf
- это вывод пользовательского интерфейса, отделенный от логики алгоритма.
do_func
будет реализован как функция-обертка вокруг реальных вызовов функций, обрабатывая результат в зависимости от результата:
(С помощью gcc -O3 do_func
встроен в вызывающую программу, поэтому нет необходимости использовать две отдельные функции)
int do_it (void)
{
if(Do1()) { return 1; };
if(Do2()) { return 2; };
if(Do3()) { return 3; };
if(Do4()) { return 4; };
if(Do5()) { return 5; };
return 0;
}
int do_func (void)
{
int result = do_it();
if(result != 0)
{
undo[result-1]();
}
return result;
}
Здесь конкретное поведение контролируется массивом undo
, который является оболочкой для различных несовместимых функций. Какие функции для вызова, в каком порядке, являются частью конкретного поведения, связанного с каждым кодом результата.
Нам нужно все привести в порядок, чтобы мы могли связать определенное поведение с определенным кодом результата. Затем, когда это необходимо, мы изменяем код только в одном месте, если поведение должно быть изменено во время обслуживания:
void Undo_stuff1 (void) { }
void Undo_stuff2 (void) { Undo1(); }
void Undo_stuff3 (void) { Undo2(); Undo1(); }
void Undo_stuff4 (void) { Undo3(); Undo2(); Undo1(); }
void Undo_stuff5 (void) { Undo4(); Undo3(); Undo2(); Undo1(); }
typedef void Undo_stuff_t (void);
static Undo_stuff_t* undo[5] =
{
Undo_stuff1,
Undo_stuff2,
Undo_stuff3,
Undo_stuff4,
Undo_stuff5,
};
MCVE:
#include <stdbool.h>
#include <stdio.h>
// some nonsense functions:
bool Do1 (void) { puts(__func__); return false; }
bool Do2 (void) { puts(__func__); return false; }
bool Do3 (void) { puts(__func__); return false; }
bool Do4 (void) { puts(__func__); return false; }
bool Do5 (void) { puts(__func__); return true; }
void Undo1 (void) { puts(__func__); }
void Undo2 (void) { puts(__func__); }
void Undo3 (void) { puts(__func__); }
void Undo4 (void) { puts(__func__); }
void Undo5 (void) { puts(__func__); }
// wrappers specifying undo behavior:
void Undo_stuff1 (void) { }
void Undo_stuff2 (void) { Undo1(); }
void Undo_stuff3 (void) { Undo2(); Undo1(); }
void Undo_stuff4 (void) { Undo3(); Undo2(); Undo1(); }
void Undo_stuff5 (void) { Undo4(); Undo3(); Undo2(); Undo1(); }
typedef void Undo_stuff_t (void);
static Undo_stuff_t* undo[5] =
{
Undo_stuff1,
Undo_stuff2,
Undo_stuff3,
Undo_stuff4,
Undo_stuff5,
};
int do_it (void)
{
if(Do1()) { return 1; };
if(Do2()) { return 2; };
if(Do3()) { return 3; };
if(Do4()) { return 4; };
if(Do5()) { return 5; };
return 0;
}
int do_func (void)
{
int result = do_it();
if(result != 0)
{
undo[result-1]();
}
return result;
}
int main (void)
{
int result = do_func();
printf("Failed %d\n", result);
}
Выход:
Do1
Do2
Do3
Do4
Do5
Undo4
Undo3
Undo2
Undo1
Failed 5
Ответ 13
typedef void(*undoer)();
int undo( undoer*const* list ) {
while(*list) {
(*list)();
++list;
}
}
void undo_push( undoer** list, undoer* undo ) {
if (!undo) return;
// swap
undoer* tmp = *list;
*list = undo;
undo = tmp;
undo_push( list+1, undo );
}
int func() {
undoer undoers[6]={0};
if (Do1()) { printf("Failed 1"); return 1; }
undo_push( undoers, Undo1 );
if (Do2()) { undo(undoers); printf("Failed 2"); return 2; }
undo_push( undoers, Undo2 );
if (Do3()) { undo(undoers); printf("Failed 3"); return 3; }
undo_push( undoers, Undo3 );
if (Do4()) { undo(undoers); printf("Failed 4"); return 4; }
undo_push( undoers, Undo4 );
if (Do5()) { undo(undoers); printf("Failed 5"); return 5; }
return 6;
}
Я заставил undo_push
выполнять работу O (n). Это менее эффективно, чем undo
выполнения O (n), так как мы ожидаем большего нажатия, чем отмены. Но эта версия была немного проще.
У более промышленной версии силы были бы указатели головы и хвоста и даже способность.
Основная идея состоит в том, чтобы сохранить очередь отмененных действий в стеке, а затем выполнить их, если вам нужно очистить.
Здесь все локально, поэтому мы не загрязняем глобальное состояние.
struct undoer {
void(*action)(void*);
void(*cleanup)(void*);
void* state;
};
struct undoers {
undoer* top;
undoer buff[5];
};
void undo( undoers u ) {
while (u.top != buff)
{
(u.top->action)(u.top->state);
if (u.top->cleanup)
(u.top->cleanup)(u.top->state);
--u.top;
}
}
void pundo(void* pu) {
undo( *(undoers*)pu );
free(pu);
}
void cleanup_undoers(undoers u) {
while (u.top != buff)
{
if (u.top->cleanup)
(u.top->cleanup)(u.top->state);
--u.top;
}
}
void pcleanup_undoers(void* pu) {
cleanup_undoers(*(undoers*)pu);
free(pu);
}
void push_undoer( undoers* to_here, undoer u ) {
if (to_here->top != (to_here->buff+5))
{
to_here->top = u;
++(to_here->top)
return;
}
undoers* chain = (undoers*)malloc( sizeof(undoers) );
memcpy(chain, to_here, sizeof(undoers));
memset(to_here, 0, sizeof(undoers));
undoer chainer;
chainer.action = pundo;
chainer.cleanup = pcleanup_undoers;
chainer.data = chain;
push_undoer( to_here, chainer );
push_undoer( to_here, u );
}
void paction( void* p ) {
(void)(*a)() = ((void)(*)());
a();
}
void push_undo( undoers* to_here, void(*action)() ) {
undor u;
u.action = paction;
u.cleanup = 0;
u.data = (void*)action;
push_undoer(to_here, u);
}
тогда вы получите:
int func() {
undoers u={0};
if (Do1()) { printf("Failed 1"); return 1; }
push_undo( &u, Undo1 );
if (Do2()) { undo(u); printf("Failed 2"); return 2; }
push_undo( &u, Undo2 );
if (Do3()) { undo(u); printf("Failed 3"); return 3; }
push_undo( &u, Undo3 );
if (Do4()) { undo(u); printf("Failed 4"); return 4; }
push_undo( &u, Undo4 );
if (Do5()) { undo(u); printf("Failed 5"); return 5; }
cleanup_undoers(u);
return 6;
}
но это становится смешным.
Ответ 14
Давайте попробуем что-нибудь с нулевыми фигурными скобками:
int result;
result = Do1() ? 1 : 0;
result = result ? result : Do2() ? 2 : 0;
result = result ? result : Do3() ? 3 : 0;
result = result ? result : Do4() ? 4 : 0;
result = result ? result : Do5() ? 5 : 0;
result > 4 ? (Undo5(),0) : 0;
result > 3 ? Undo4() : 0;
result > 2 ? Undo3() : 0;
result > 1 ? Undo2() : 0;
result > 0 ? Undo1() : 0;
result ? printf("Failed %d\r\n", result) : 0;
Да. 0;
является допустимым утверждением в C (и C++). В случае, если некоторые функции возвращают что-то несовместимое с этим синтаксисом (например, возможно, void), можно использовать стиль Undo5().
Ответ 15
Разумный (без gotos, без вложенных или цепочечных ifs) подход будет
int bar(void)
{
int rc = 0;
do
{
if (do1())
{
rc = 1;
break;
}
if (do2())
{
rc = 2;
break;
}
...
if (do5())
{
rc = 5;
break;
}
} while (0);
if (rc)
{
/* More or less stolen from Chris' answer:
https://stackoverflow.com/a/53444967/694576) */
switch(rc - 1)
{
case 5: /* Not needed for this example, but left in in case we'd add do6() ... */
undo5();
case 4:
undo4();
case 3:
undo3();
case 2:
undo2();
case 1:
undo1();
default:
break;
}
}
return rc;
}