Подтвердить что ты не робот

Элегантный способ выхода из функции аккуратно без использования goto в C

Мы часто пишем некоторые функции, которые имеют более одной точки выхода (т.е. return в C). В то же время, при выходе из функции, для некоторых общих работ, таких как очистка ресурсов, мы хотим реализовать их только один раз, а не реализовывать их в каждой точке выхода. Как правило, мы можем достичь нашего желания, используя goto следующим образом:

void f()
{
    ...
    ...{..{... if(exit_cond) goto f_exit; }..}..
    ...
    f_exit:
    some general work such as cleanup
}

Я думаю, что использование goto здесь приемлемо, и я знаю, что многие люди соглашаются на использование goto здесь. Просто из любопытства, существует ли какой-нибудь элегантный способ аккуратного выхода из функции без использования goto в C?

4b9b3361

Ответ 1

Зачем избегать goto?

Проблема, которую вы хотите решить: Как убедиться, что какой-то общий код всегда выполняется, прежде чем функция вернется к вызывающему?. Это проблема для программистов на C, поскольку C не предоставляет никаких встроенная поддержка RAII.

Как вы уже уступаете в своем теле вопроса, goto является вполне приемлемым решением. Тем не менее, могут быть нетехнические причины, чтобы избежать его использования:

  • академическое упражнение
  • соответствие стандарту кодирования
  • личная прихоть (которая, по моему мнению, является мотивирующей этот вопрос)

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

Явное вызов функции очистки

Если избежать явного перехода (например, goto или break), общий код очистки может быть инкапсулирован внутри функции и явно вызван в начале раннего return.

int foo () {
    ...
    if (SOME_ERROR) {
        return foo_cleanup(SOME_ERROR_CODE, ...);
    }
    ...
}

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

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

Добавьте еще один слой косвенности.

Без изменения семантики пользовательского API измените его реализацию на оболочку, состоящую из двух частей. Часть первая выполняет фактическую работу функции. Часть вторая выполняет очистку, необходимую после завершения первой части. Если каждая часть инкапсулирована в пределах своей собственной функции, функция-оболочка имеет очень чистую реализацию.

struct bar_stuff {...};

static int bar_work (struct bar_stuff *stuff) {
    ...
    if (SOME_ERROR) return SOME_ERROR_CODE;
    ...
}

int bar () {
    struct bar_stuff stuff = {};
    int r = bar_work(&stuff);
    return bar_cleanup(r, &stuff);
}

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

Разное...

Можно рассмотреть более эзотерические решения с использованием setjmp()/longjmp(), но их правильное использование может быть затруднено. Существуют оболочки с открытым исходным кодом, которые реализуют над ними макросы стиля обработки исключений try/catch (например, cexcept), но вы должны изменить свой стиль кодирования для использования этот стиль для обработки ошибок.

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

Сделайте, как делают римляне.

Если вам необходимо соблюдать стандарты кодирования, то подход лучший должен следовать любому методу, который наиболее распространен в существующей базе кода. Это касается почти всех аспектов внесения изменений в существующую стабильную базу исходного кода. Было бы расстроено вводить новый стиль кодирования. Вы должны искать одобрение от полномочий, которые были бы, если вы считаете, что изменение резко улучшит некоторые аспекты программного обеспечения. В противном случае, поскольку "элегантность" субъективна, аргументация ради "элегантности" никуда не приведет.

Ответ 2

Например

void f()
{
    do
    {
         ...
         ...{..{... if(exit_cond) break; }..}..
         ...
    }  while ( 0 );

    some general work such as cleanup
}

Или вы можете использовать следующую структуру

while ( 1 )
{
   //...
}

Основным преимуществом структурного подхода, противоположного использованию операторов goto, является то, что он вводит дисциплину при написании кода.

Я уверен и имею достаточно опыта, что если функция имеет одну инструкцию goto, то через некоторое время она будет иметь несколько операторов goto.:)

Ответ 3

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

Я считаю, что самый уродливый способ:

int func (void)
{
  if(some_error)
  {
    cleanup();
    return result;
  }

  ...

  if(some_other_error)
  {
    cleanup();
    return result;
  }

  ...

  cleanup();
  return result;
}

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

Ответ 4

Я думаю, что этот вопрос очень интересный, но на него нельзя ответить без влияния субъективности, потому что элегантность субъективна. Мои идеи по этому поводу заключаются в следующем: В общем, то, что вы хотите сделать в описываемом вами сценарии, состоит в том, чтобы не допустить, чтобы управление проходило через ряд операторов вдоль пути выполнения. Другие языки будут делать это путем создания исключения, которое вам придется поймать.

Я уже записал аккуратные хаки, чтобы делать то, что вы хотите сделать, в основном, в каждом контрольном выражении, которое есть на C, иногда в комбинации, но я думаю, что все они просто очень неясные способы выразить идею перехода на особый пункт. Вместо этого я просто остановлюсь на том, как мы достигнем точки , где goto может быть предпочтительнее. Еще раз, то, что вы хотите выразить, - это то, что произошло, что предотвращает выполнение обычного пути выполнения. Что-то, что не просто обычное условие, которое может быть обработано путем переноса другой ветки по пути, но что-то делает невозможным безопасное использование пути к регулярной точке возврата в текущем состоянии, Я думаю, что в этой ситуации есть три варианта:

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

Если ваша очистка достаточно похожа на любой возможный экстренный выход, я бы предпочел goto, потому что запись кода избыточно просто загромождает функцию. Я думаю, вы должны торговать числом точек возврата и реплицированным кодом очистки, который вы создаете, против неловкости использования goto. Оба решения должны приниматься как личный выбор программиста, если нет серьезных причин не делать этого, например. вы согласились, что все функции должны иметь один выход. Тем не менее, использование либо должно быть последовательным и последовательным в коде. Третья альтернатива - имо - менее удобочитаемый двоюродный брат goto, потому что, в конце концов, вы перейдете к набору процедур очистки, возможно, заключенных с помощью else-statement тоже, но это делает намного труднее для людей следовать обычным поток вашей программы из-за глубокого вложения условных утверждений.

tl; dr: Я думаю, что выбор между условным возвратом и goto на основе последовательных решений стиля является самым изящным способом, потому что это самый выразительный способ представить ваши идеи за кодом, а ясность - элегантность.

Ответ 5

Я предполагаю, что элегантный может означать для вас странный и что вы просто хотите избежать ключевого слова goto, поэтому....

Вы можете использовать setjmp (3) и longjmp:

void foo() {
jmp_buf jb;
if (setjmp(jb) == 0) {
   some_stuff();
   //// etc...
   if (bad_thing() {
       longjmp(jb, 1);
   }
 };
};

Я понятия не имею, соответствует ли это вашим критериям элегантности. (Я считаю, что это не очень элегантно, но это только мнение, но явного goto) не существует.

Однако интересно, что longjmp - нелокальный скачок: вы могли бы пройти (косвенно) jb до some_stuff и иметь некоторую другую процедуру (например, вызываемую some_stuff), сделать longjmp. Это может стать нечитаемым кодом (так прокомментируйте его с умом).

Даже более уродливый, чем longjmp: используйте (в Linux) setcontext (3)

Прочитайте продолжения и исключениявызов /cc в схеме).

И, конечно, стандартный exit (3) - изящный (и полезный) способ выйти из некоторой функции. Иногда вы можете играть в аккуратный трюк, используя atexit (3)

Кстати, код ядра Linux использует довольно часто goto, в том числе в некотором коде, который считается элегантным.

Моей точкой является: IMHO не фанатично против goto -s, так как есть случаи, когда использование (с осторожностью) на самом деле элегантно.

Ответ 6

Я поклонник:

void foo(exp) 
{
    if(    ate_breakfast(exp)
        && tied_shoes(exp)
        && finished_homework(exp)
      )
    {
        good_to_go(exp);
    }
    else
    {
        fix_the_problems(exp);
    }
}

Где ate_breakfast, tied_shoes и finished_homework берут указатель на exp, на котором они работают, и возвращают bools, указывающие на сбой этого конкретного теста.

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

Ответ 7

инструкция goto никогда не нужна, также проще писать код, не используя его.

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

if(exit_cond) {
    clean_the_mess();
    return;
}

Или вы можете сломаться, как сказал Влад. Но один из недостатков этого, если ваш цикл имеет глубоко вложенную структуру, будет выходить только из самого внутреннего цикла. Например:

While ( exp ) {
    for (exp; exp; exp) {
        for (exp; exp; exp) {
            if(exit_cond) {
                clean_the_mess();
                break;
            }
        }
    }
}

выйдет только из внутреннего цикла и не откажется от процесса.