Як говорили інші, проблема не в goto
самій собі; проблема полягає в тому, як люди користуються goto
, і як це може зробити текст важче зрозуміти та підтримувати.
Припустимо такий фрагмент коду:
i = 4;
label: printf( "%d\n", i );
Яке значення надрукується i
? Коли він друкується? Поки ви не будете обліковувати кожен екземпляр goto label
своєї функції, ви не можете знати. Проста наявність цієї мітки руйнує вашу здатність налагоджувати код простим оглядом. Для невеликих функцій з однією або двома гілками не велика проблема. Для не малих функцій ...
Ще на початку 90-х нам дали купу коду С, який наніс 3d графічний дисплей і сказав, щоб змусити його працювати швидше. Це було лише близько 5000 рядків коду, але все це було main
, і автор використав близько 15 goto
розгалужень в обох напрямках. Це було поганим кодом для початку, але присутність цих людей goto
зробила це набагато гірше. Моєму співробітникові знадобилося близько 2 тижнів, щоб розгадати потік контролю. Ще краще, що вони goto
призвели до того, що код настільки щільно поєднався з самим собою, що ми не могли вносити жодних змін, нічого не порушуючи.
Ми спробували компілювати оптимізацію рівня 1, і компілятор з’їв всю наявну оперативну пам’ять, потім всю наявну підкачку, а потім запанікував систему (яка, мабуть, не мала нічого спільного з goto
самими s, але мені подобається кидати цей анекдот).
Зрештою, ми дали замовникові два варіанти - давайте перепишемо всю справу з нуля, або купимо швидше обладнання.
Вони купували швидше обладнання.
Правила Bode щодо використання goto
:
- Гілка лише вперед;
- Не обминайте керуючі структури (тобто, не розгалужуйте в тіло
if
або for
або while
заяву);
- Не використовуйте
goto
замість структури управління
Бувають випадки, коли а goto
- правильна відповідь, але вони рідкісні (виривання з глибоко вкладеного циклу - це єдине місце, де я б його використовував).
EDIT
Розширюючи цю останню заяву, ось один з небагатьох дійсних випадків використання для goto
. Припустимо, у нас є така функція:
T ***myalloc( size_t N, size_t M, size_t P )
{
size_t i, j, k;
T ***arr = malloc( sizeof *arr * N );
for ( i = 0; i < N; i ++ )
{
arr[i] = malloc( sizeof *arr[i] * M );
for ( j = 0; j < M; j++ )
{
arr[i][j] = malloc( sizeof *arr[i][j] * P );
for ( k = 0; k < P; k++ )
arr[i][j][k] = initial_value();
}
}
return arr;
}
Тепер у нас є проблема - що робити, якщо один із malloc
дзвінків не завершиться посередині? Навряд чи така подія може бути, ми не хочемо повертати частково виділений масив, а також не хочемо просто виконувати функцію з помилкою; ми хочемо прибрати за собою і розмістити будь-яку частково виділену пам'ять. Мова, яка кидає виняток на поганий аллокал, це досить просто - ви просто пишете обробник винятків, щоб звільнити те, що вже було виділено.
У C у вас немає структурованої обробки винятків; ви повинні перевірити значення повернення кожного malloc
дзвінка і вжити відповідних дій.
T ***myalloc( size_t N, size_t M, size_t P )
{
size_t i, j, k;
T ***arr = malloc( sizeof *arr * N );
if ( arr )
{
for ( i = 0; i < N; i ++ )
{
if ( !(arr[i] = malloc( sizeof *arr[i] * M )) )
goto cleanup_1;
for ( j = 0; j < M; j++ )
{
if ( !(arr[i][j] = malloc( sizeof *arr[i][j] * P )) )
goto cleanup_2;
for ( k = 0; k < P; k++ )
arr[i][j][k] = initial_value();
}
}
}
goto done;
cleanup_2:
// We failed while allocating arr[i][j]; clean up the previously allocated arr[i][j]
while ( j-- )
free( arr[i][j] );
free( arr[i] );
// fall through
cleanup_1:
// We failed while allocating arr[i]; free up all previously allocated arr[i][j]
while ( i-- )
{
for ( j = 0; j < M; j++ )
free( arr[i][j] );
free( arr[i] );
}
free( arr );
arr = NULL;
done:
return arr;
}
Чи можемо ми це зробити без використання goto
? Звичайно, ми можемо - це просто вимагає трохи додаткової бухгалтерії (і, на практиці, це був би шлях). Але якщо ви шукаєте місця, де використання goto
не відразу є ознакою поганої практики чи дизайну, це одна з небагатьох.