Как избежать использования goto и эффективно разбить вложенные циклы
Я бы сказал, что факт, что использование goto
считается плохой практикой, когда дело доходит до программирования на C/C++.
Однако, учитывая следующий код
for (i = 0; i < N; ++i)
{
for (j = 0; j < N; j++)
{
for (k = 0; k < N; ++k)
{
...
if (condition)
goto out;
...
}
}
}
out:
...
Интересно, как эффективно добиться такого же поведения, не используя goto
. Я имею в виду, что мы могли бы сделать что-то вроде проверки condition
в конце каждого цикла, например, но AFAIK goto будет генерировать только одну инструкцию сборки, которая будет jmp
. Так что это самый эффективный способ сделать это, о котором я могу думать.
Есть ли другие, которые считаются хорошей практикой? Я ошибаюсь, когда говорю, что считать неудачной практикой использовать goto? Если да, то это будет один из тех случаев, когда это хорошо использовать?
Спасибо
Ответы
Ответ 1
Лучшая версия non-goto (imo) будет выглядеть примерно так:
void calculateStuff()
{
// Please use better names than this.
doSomeStuff();
doLoopyStuff();
doMoreStuff();
}
void doLoopyStuff()
{
for (i = 0; i < N; ++i)
{
for (j = 0; j < N; j++)
{
for (k = 0; k < N; ++k)
{
/* do something */
if (/*condition*/)
return; // Intuitive control flow without goto
/* do something */
}
}
}
}
Разделение этого также, вероятно, является хорошей идеей, потому что это помогает вам держать свои функции короткими, ваш код читабельным (если вы называете функции лучше, чем я) и низкими зависимостями.
Ответ 2
Если у вас есть глубоко вложенные циклы, подобные этому, и вы должны вырваться, я считаю, что goto
- лучшее решение. Некоторые языки (а не C) имеют оператор break(N)
который выйдет из более чем одного цикла. Причина C не в том, что это даже хуже, чем goto
: вы должны подсчитывать вложенные циклы, чтобы выяснить, что он делает, и он уязвим для кого-то, входящего позже, и добавления или удаления уровня гнездования, не замечая что счет прерывания должен быть скорректирован.
Да, gotos
как правило, нахмурились. Использование goto
здесь не является хорошим решением; это всего лишь наименьшее из нескольких зол.
В большинстве случаев причина, по которой вы должны выйти из глубоко вложенного цикла, состоит в том, что вы что-то ищете, и вы его нашли. В этом случае (и, как было предложено несколько других комментариев и ответов), я предпочитаю перемещать вложенный цикл в свою собственную функцию. В этом случае return
из внутреннего цикла выполняет вашу задачу очень чисто.
(Есть те, кто говорит, что функции всегда должны возвращаться в конце, а не из середины. Эти люди скажут, что решение "просто-напросто-к-функции" поэтому недействительно, и они заставили бы использовать из того же неудобного метода (ов) с нарушением внутреннего цикла, даже когда поиск был разделен на его собственную функцию. Лично я считаю, что эти люди ошибаются, но ваш пробег может отличаться.)
Если вы настаиваете на том, чтобы не использовать goto
, и если вы настаиваете на том, чтобы не использовать отдельную функцию с ранним возвратом, то да, вы можете сделать что-то вроде поддержания дополнительных контрольных переменных Boolean и проверки их избыточно в состоянии управления каждого вложенного цикла, но это всего лишь неприятность и беспорядок. (Это одно из самых больших явлений, о которых я говорил, используя простой goto
, меньше).
Ответ 3
Я думаю, что goto
- это совершенно нормальная вещь, и это один из его исключительных случаев использования в соответствии с основными принципами C++.
Однако, возможно, еще одно решение, которое следует рассмотреть, - это лямбда IIFE. По-моему, это немного более элегантно, чем объявление отдельной функции!
[&] {
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; j++)
for (int k = 0; k < N; ++k)
if (condition)
return;
}();
Спасибо JohnMcPineapple за reddit за это предложение!
Ответ 4
В этом случае вы не хотите избегать использования goto
.
В общем случае следует избегать использования goto
, однако есть исключения из этого правила, и ваш случай является хорошим примером одного из них.
Давайте рассмотрим альтернативы:
for (i = 0; i < N; ++i) {
for (j = 0; j < N; j++) {
for (k = 0; k < N; ++k) {
...
if (condition)
break;
...
}
if (condition)
break;
}
if (condition)
break;
}
Или же:
int flag = 0
for (i = 0; (i < N) && !flag; ++i) {
for (j = 0; (j < N) && !flag; j++) {
for (k = 0; (k < N) && !flag; ++k) {
...
if (condition) {
flag = 1
break;
...
}
}
}
Ни один из них не является столь кратким или читаемым, как версия goto
.
Использование goto
считается приемлемым в тех случаях, когда вы только прыгаете вперед (а не назад), и это делает ваш код более понятным и понятным.
Если, с другой стороны, вы используете goto
для перехода в обоих направлениях или для перехода в область, которая потенциально может обойти переменную инициализацию, это было бы плохо.
Здесь плохой пример goto
:
int x;
scanf("%d", &x);
if (x==4) goto bad_jump;
{
int y=9;
// jumping here skips the initialization of y
bad_jump:
printf("y=%d\n", y);
}
Компилятор C++ выдает ошибку здесь, потому что goto
перескакивает через инициализацию y
. Компиляторы C, однако, скомпилируют это, и вышеприведенный код вызовет неопределенное поведение при попытке распечатать y
который будет неинициализирован, если произойдет goto
.
Другим примером правильного использования goto
является обработка ошибок:
void f()
{
char *p1 = malloc(10);
if (!p1) {
goto end1;
}
char *p2 = malloc(10);
if (!p2) {
goto end2;
}
char *p3 = malloc(10);
if (!p3) {
goto end3;
}
// do something with p1, p2, and p3
end3:
free(p3);
end2:
free(p2);
end1:
free(p1);
}
Это выполняет всю очистку в конце функции. Сравните это с альтернативой:
void f()
{
char *p1 = malloc(10);
if (!p1) {
return;
}
char *p2 = malloc(10);
if (!p2) {
free(p1);
return;
}
char *p3 = malloc(10);
if (!p3) {
free(p2);
free(p1);
return;
}
// do something with p1, p2, and p3
free(p3);
free(p2);
free(p1);
}
Если очистка выполняется в нескольких местах. Если впоследствии вы добавите дополнительные ресурсы, которые необходимо очистить, вы должны помнить, что нужно добавлять очистку во все эти места, а также очищать любые ресурсы, которые были получены ранее.
Вышеприведенный пример более важен для C, чем C++, поскольку в последнем случае вы можете использовать классы с надлежащими деструкторами и интеллектуальными указателями, чтобы избежать ручной очистки.
Ответ 5
Альтернатива - 1
Вы можете сделать что-то вроде следующего:
- Установите переменную
bool
в начале isOkay = true
- Все ваши
for
условий цикла, добавить дополнительное условие isOkay == true
- Когда ваше пользовательское условие выполнено/не выполнено, установите
isOkay = false
.
Это заставит ваши циклы остановиться. Тем не менее, дополнительная переменная bool
будет удобна.
bool isOkay = true;
for (int i = 0; isOkay && i < N; ++i)
{
for (int j = 0; isOkay && j < N; j++)
{
for (int k = 0; isOkay && k < N; ++k)
{
// some code
if (/*your condition*/)
isOkay = false;
}
}
}
Альтернатива - 2
Во-вторых. если вышеупомянутые итерации цикла находятся в функции, лучший выбор - return
результат, когда пользовательское условие выполняется.
bool loop_fun(/* pass the array and other arguments */)
{
for (int i = 0; i < N ; ++i)
{
for (int j = 0; j < N ; j++)
{
for (int k = 0; k < N ; ++k)
{
// some code
if (/* your condition*/)
return false;
}
}
}
return true;
}
Ответ 6
Lambdas позволяет создавать локальные области:
[&]{
for (i = 0; i < N; ++i)
{
for (j = 0; j < N; j++)
{
for (k = 0; k < N; ++k)
{
...
if (condition)
return;
...
}
}
}
}();
если вы также хотите получить возможность возврата из этой области:
if (auto r = [&]()->boost::optional<RetType>{
for (i = 0; i < N; ++i)
{
for (j = 0; j < N; j++)
{
for (k = 0; k < N; ++k)
{
...
if (condition)
return {};
...
}
}
}
}()) {
return *r;
}
где boost::nullopt
{}
или boost::nullopt
- это "разрыв", а возвращаемое значение возвращает значение из охватывающей области.
Другой подход:
for( auto idx : cube( {0,N}, {0,N}, {0,N} ) {
auto i = std::get<0>(idx);
auto j = std::get<1>(idx);
auto k = std::get<2>(idx);
}
где мы создаем итерабельность по всем 3 измерениям и делаем его 1 глубокой вложенной петлей. Теперь break
работает отлично. Вам нужно написать cube
.
В c++17 это становится
for( auto[i,j,k] : cube( {0,N}, {0,N}, {0,N} ) ) {
}
что приятно.
Теперь, в приложении, в котором вы должны реагировать, цикл на большом 3-мерном диапазоне при уровне управления начальным уровнем управления часто является плохой идеей. Вы можете отключить его, но даже тогда у вас возникает проблема с тем, что поток работает слишком долго. И большинство трехмерных больших итераций, с которыми я играл, могут извлечь выгоду из использования самих подзадач.
С этой целью вы в конечном итоге хотите классифицировать свою операцию на основе того, какие данные она обращается, а затем передать вашу операцию тому, что планирует итерацию для вас.
auto work = do_per_voxel( volume,
[&]( auto&& voxel ) {
// do work on the voxel
if (condition)
return Worker::abort;
else
return Worker::success;
}
);
то задействуемый поток управления переходит в функцию do_per_voxel
.
do_per_voxel
не будет простой голой петлей, а скорее системой переписывания задач на вокселе в задачи на сканирование (или даже задачи на плоскости в зависимости от того, насколько велики плоскости/строки сканирования во время выполнения (!)) затем отправляйте их по очереди в планировщик заданных задач с пулом потоков, сшивайте результирующие ручки задач и возвращайте будущую work
которую можно ожидать или использовать в качестве триггера продолжения, когда работа выполняется.
И иногда вы просто используете goto. Или вы вручную выполняете функции для подлупсов. Или вы используете флаги, чтобы вырваться из глубокой рекурсии. Или вы помещаете весь цикл 3 слоя в свою собственную функцию. Или вы создаете операторов цикла, используя библиотеку монад. Или вы можете выбросить исключение (!) И поймать его.
Ответ на почти каждый вопрос в c++ - "это зависит". Объем проблемы и количество доступных вами технологий являются большими, а детали проблемы изменяют детали решения.
Ответ 7
Разделите свои функции на функции. Это значительно упрощает понимание, потому что теперь вы можете видеть, что делает каждый цикл.
bool doHerpDerp() {
for (i = 0; i < N; ++i)
{
if (!doDerp())
return false;
}
return true;
}
bool doDerp() {
for (int i=0; i<X; ++i) {
if (!doHerp())
return false;
}
return true;
}
bool doHerp() {
if (shouldSkip)
return false;
return true;
}
Ответ 8
Есть ли другие, которые считаются хорошей практикой? Я ошибаюсь, когда говорю, что считать неудачной практикой использовать goto?
goto
можно злоупотреблять и злоупотреблять, но я не вижу ни одного из них в вашем примере. Вывод из глубоко вложенной циклы наиболее четко выражен простой goto label_out_of_the_loop;
,
Плохая практика - использовать многие goto
которые переходят на разные метки, но в таких случаях это не ключевое слово goto
, которое делает ваш код плохим. Это тот факт, что вы прыгаете в коде, что затрудняет его выполнение, что делает его плохим. Если, однако, вам нужен один прыжок из вложенных циклов, то почему бы не использовать инструмент, созданный именно для этой цели.
Чтобы использовать составленный из тонкой воздушной аналогии: Представьте, что вы живете в мире, где в прошлом было бедро забивать гвозди в стены. В последнее время стало более утонченным для сверления винтов в стены с помощью отверток и молотков, которые полностью вышли из моды. Теперь подумайте, что вам нужно (несмотря на то, что вы немного старомодны), приложите гвоздь к стене. Вы не должны воздерживаться от использования молотка для этого, но, может быть, вам лучше спросить себя, действительно ли вам нужен гвоздь в стене вместо винта.
(На всякий случай это не ясно: молоток - это goto
а гвоздь в стене - это прыжок из вложенной циклы, в то время как винт в стене будет использовать функции, чтобы избежать глубокого гнездования alltogether;)
Ответ 9
Один из возможных способов - присвоить логическое значение переменной, которая представляет состояние. Это состояние позже может быть протестировано с использованием условного оператора "IF" для других целей позже в коде.
Ответ 10
насколько ваш комментарий об эффективности компиляции обеих опций в режиме выпуска на visual studio 2017 дает точно такую же сборку.
for (int i = 0; i < 5; ++i)
{
for (int j = 0; j < 5; ++j)
{
for (int k = 0; k < 5; ++k)
{
if (i == 1 && j == 2 && k == 3) {
goto end;
}
}
}
}
end:;
и с флагом.
bool done = false;
for (int i = 0; i < 5; ++i)
{
for (int j = 0; j < 5; ++j)
{
for (int k = 0; k < 5; ++k)
{
if (i == 1 && j == 2 && k == 3) {
done = true;
break;
}
}
if (done) break;
}
if (done) break;
}
оба производят..
xor edx,edx
xor ecx,ecx
xor eax,eax
cmp edx,1
jne main+15h (0C11015h)
cmp ecx,2
jne main+15h (0C11015h)
cmp eax,3
je main+27h (0C11027h)
inc eax
cmp eax,5
jl main+6h (0C11006h)
inc ecx
cmp ecx,5
jl main+4h (0C11004h)
inc edx
cmp edx,5
jl main+2h (0C11002h)
поэтому нет никакой выгоды. Другой вариант, если вы используете современный компилятор c++, должен обернуть его в лямбда.
[](){
for (int i = 0; i < 5; ++i)
{
for (int j = 0; j < 5; ++j)
{
for (int k = 0; k < 5; ++k)
{
if (i == 1 && j == 2 && k == 3) {
return;
}
}
}
}
}();
снова это создает ту же самую сборку. Лично я думаю, что использование goto в вашем примере вполне приемлемо. Понятно, что происходит с кем-то еще, и делает более сжатый код. Вероятно, лямбда одинаково кратка.
Ответ 11
Конкретный
IMO, в этом конкретном примере, я думаю, что важно заметить общую функциональность между вашими циклами. (Теперь я знаю, что ваш пример не обязательно является буквальным здесь, а просто переносите меня в течение секунды), поскольку каждый цикл повторяется N
раз, вы можете перестроить свой код следующим образом:
пример
int max_iterations = N * N * N;
for (int i = 0; i < max_iterations; i++)
{
/* do stuff, like the following for example */
*(some_ptr + i) = 0; // as opposed to *(some_3D_ptr + i*X + j*Y + Z) = 0;
// some_arr[i] = 0; // as oppose to some_3D_arr[i][j][k] = 0;
}
Теперь важно помнить, что все циклы, в то время как для или иначе, на самом деле являются просто синтаксическим сахаром для парадигмы if-goto. Я согласен с остальными, что вы должны иметь функцию, возвращающую результат, однако я хотел показать пример, подобный приведенному выше, в котором это может быть не так. Конечно, я бы назвал это выше в обзоре кода, но если вы заменили вышеперечисленное, я бы подумал, что это шаг в неправильном направлении. (ПРИМЕЧАНИЕ. - Убедитесь, что вы можете надежно вставить его в нужный тип данных)
генеральный
Теперь, как общий ответ, условия выхода для вашего цикла могут быть не одинаковыми каждый раз (например, это сообщение). Как правило, вытаскивайте столько ненужных операций из ваших циклов (умножений и т.д.) Настолько, насколько это возможно, поскольку компиляторы становятся умнее каждый день, нет замены для написания эффективного и удобочитаемого кода.
пример
/* matrix_length: int of m*n (row-major order) */
int num_squared = num * num;
for (int i = 0; i < matrix_length; i++)
{
some_matrix[i] *= num_squared; // some_matrix is a pointer to an array of ints of size matrix_length
}
вместо того, чтобы писать *= num * num
, нам больше не нужно полагаться на компилятор, чтобы оптимизировать это для нас (хотя любой хороший компилятор должен). Таким образом, любые двойные или триплексные вложенные циклы, которые выполняют вышеуказанную функциональность, также принесут пользу не только вашему коду, но и IMO, написав чистый и эффективный код с вашей стороны. В первом примере мы могли бы иметь *(some_3D_ptr + i*X + j*Y + Z) = 0;
! Доверяем ли мы компилятору оптимизировать i*X
и j*Y
, чтобы они не вычислялись на каждой итерации?
bool check_threshold(int *some_matrix, int max_value)
{
for (int i = 0; i < rows; i++)
{
int i_row = i*cols; // no longer computed inside j loop unnecessarily.
for (int j = 0; j < cols; j++)
{
if (some_matrix[i_row + j] > max_value) return true;
}
}
return false;
}
Тьфу! Почему мы не используем классы, предоставляемые STL или библиотекой типа Boost? (здесь мы должны делать код низкого уровня/высокого качества). Из-за сложности я даже не мог написать 3D-версию. Несмотря на то, что у нас есть что-то в руке, может быть даже лучше использовать #pragma unroll или аналогичные подсказки препроцессора, если ваш компилятор разрешает.
Заключение
Как правило, чем выше уровень абстракции, который вы можете использовать, тем лучше, однако, если сказать, что сглаживание 1-мерной матрицы строковых чисел с целыми числами в 2-мерный массив делает ваш поток кода более сложным для понимания/расширения, стоит ли это? Аналогично, это также может быть индикатором, чтобы сделать что-то в своей собственной функции. Надеюсь, что, учитывая эти примеры, вы можете увидеть, что разные парадигмы призваны в разных местах, и ваша работа в качестве программиста - понять это. Не сходите с ума от вышеуказанного, но убедитесь, что вы знаете, что они означают, как их использовать, и когда их требуют, и, самое главное, убедитесь, что другие люди, использующие вашу кодовую базу, знают, что они собой представляют, и никаких сомнений в этом. Удачи!
Ответ 12
bool meetCondition = false;
for (i = 0; i < N && !meetCondition; ++i)
{
for (j = 0; j < N && !meetCondition; j++)
{
for (k = 0; k < N && !meetCondition; ++k)
{
...
if (condition)
meetCondition = true;
...
}
}
}
Ответ 13
Есть уже несколько отличных ответов, которые расскажут вам, как вы можете реорганизовать свой код, поэтому я не буду их повторять. Нет необходимости более точно кодировать этот способ для повышения эффективности; вопрос в том, не является ли его неэлегантным. (Хорошо, одно уточнение Ill предлагает: если ваши вспомогательные функции только когда-либо предназначены для использования внутри тела этой одной функции, вы можете помочь оптимизатору, объявив их static
, поэтому он наверняка знает, что функция не имеет внешних связь и никогда не будут вызываться из любого другого модуля, и намек inline
косяка боли. Однако, предыдущие ответы сказать, что, когда вы используете лямбду, современные компиляторы не нужны какие - либо подобные намеки.)
Я собираюсь немного оспаривать постановку вопроса. Вы правы, что большинство программистов имеют запрет на использование goto
. Это, на мой взгляд, упустило из виду первоначальную цель. Когда Эдсберг Дейкстра писал: "Перейти к заявлению считается вредным", была определенная причина, по которой он так думал: "необузданное" использование go to
делает слишком трудным рассуждать о текущем состоянии программы, и какие условия должны быть истинными, по сравнению с потоком управления от рекурсивных вызовов функций (которые он предпочитал) или итеративных циклов (которые он принял). Он заключил:
Переход go to
утверждению в его нынешнем виде слишком примитивен; это слишком много приглашение сделать беспорядок одной программы. Можно рассматривать и оценивать предложения, рассматриваемые как обуздание его использования. Я не утверждаю, что упомянутые положения являются исчерпывающими в том смысле, что они будут удовлетворять все потребности, но независимо от того, какие предложения предлагаются (например, предложения об абортах), они должны удовлетворять требованию о том, чтобы независимую систему программистов можно было поддерживать для описания процесса в полезный и управляемый способ.
Многие языки программирования C, например Rust и Java, имеют дополнительное "предложение, рассматриваемое как препятствие его использованию", break
с меткой. Еще более ограниченный синтаксис может быть чем-то вроде break 2 continue;
для выхода из двух уровней вложенного цикла и возобновления в верхней части цикла, содержащего их. Это больше не проблема, чем break
в стиле C, на что Дийкстра хотел сделать: определение краткого описания состояния программы, которое программисты могут отслеживать в своих головах, или статический анализатор найдет приемлемым.
Ограничение goto
к конструкциям, как это делает его просто переименованный перерыв на этикетку. Оставшаяся проблема заключается в том, что компилятор и программист не обязательно знают, что вы собираетесь использовать его именно так.
Если theres важное пост-условие, которое выполняется после цикла, и ваша забота о goto
такая же, как Dijkstras, вы можете подумать об этом в коротком комментарии, например //We have done foo to every element, or encountered condition and stopped.
Это облегчит проблему для людей, и статический анализатор должен преуспеть.
Ответ 14
Лучшее решение состоит в том, чтобы поместить циклы в функцию и затем return
из этой функции.
Это, по сути то же самое, как ваш goto
пример, но с массивным преимуществом, что вам избежать еще одной goto
дискуссии.
Упрощенный псевдокод:
bool function (void)
{
bool result = something;
for (i = 0; i < N; ++i)
for (j = 0; j < N; j++)
for (k = 0; k < N; ++k)
if (condition)
return something_else;
...
return result;
}
Другое преимущество здесь в том, что вы можете перейти с bool
на enum
если вы столкнетесь с более чем двумя сценариями. Вы не можете сделать это с goto
в читаемом виде. В тот момент, когда вы начинаете использовать множественные gotos и несколько ярлыков, это момент, когда вы используете кодировку спагетти. Да, даже если вы просто переходите вниз - читать и поддерживать не будет.
Примечательно, что если у вас есть три вложенных цикла, это может свидетельствовать о том, что вы должны попытаться разбить свой код на несколько функций, и тогда вся эта дискуссия может быть даже не актуальной.