Jak powiedzieli inni, problem nie dotyczy goto
samego siebie; problem polega na tym, jak ludzie używają goto
i jak może to utrudnić zrozumienie i utrzymanie kodu.
Załóżmy następujący fragment kodu:
i = 4;
label: printf( "%d\n", i );
Dla jakiej wartości drukowane i
? Kiedy zostanie wydrukowany? Dopóki nie rozliczysz każdego wystąpienia goto label
funkcji, nie możesz tego wiedzieć. Prosta obecność tej etykiety niszczy możliwość debugowania kodu przez prostą kontrolę. W przypadku małych funkcji z jedną lub dwiema gałęziami nie stanowi większego problemu. Dla niemałych funkcji ...
We wczesnych latach 90. dostaliśmy stos kodu C, który napędzał trójwymiarowy wyświetlacz graficzny i kazano mu działać szybciej. Było tylko około 5000 linii kodu, ale wszystko było w porządku main
, a autor wykorzystał około 15 goto
gałęzi w obu kierunkach. Na początku był to zły kod, ale obecność tych kodów goto
znacznie go pogorszyła. Około 2 tygodni zajęło mojemu współpracownikowi ustalenie przepływu kontroli. Co więcej, goto
spowodowało to, że kod był tak ściśle powiązany z samym sobą, że nie mogliśmy dokonywać żadnych zmian bez zepsucia czegoś.
Próbowaliśmy kompilować z optymalizacją na poziomie 1, a kompilator zjadł całą dostępną pamięć RAM, następnie całą dostępną wymianę, a następnie spanikował system (co prawdopodobnie nie miało nic wspólnego z samym goto
sobą, ale lubię wyrzucać tę anegdotę).
Na koniec daliśmy klientowi dwie opcje - przepiszmy wszystko od zera lub kupmy szybszy sprzęt.
Kupili szybszy sprzęt.
Zasady Bode dotyczące używania goto
:
- Tylko oddział do przodu;
- Nie obejście struktur kontrolnych (czyli, nie rozgałęzia się w ciało
if
lub for
lub while
rachunku);
- Nie stosować
goto
zamiast struktury kontrolnej
Są przypadki, w których a goto
jest właściwą odpowiedzią, ale są rzadkie (wyrwanie się z głęboko zagnieżdżonej pętli jest jedynym miejscem, w którym mógłbym jej użyć).
EDYTOWAĆ
Rozwijając ostatnie zdanie, oto jeden z niewielu prawidłowych przypadków użycia dla goto
. Załóżmy, że mamy następującą funkcję:
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;
}
Mamy teraz problem - co jeśli jedno z malloc
połączeń zakończy się niepowodzeniem w połowie? Wydaje się to mało prawdopodobne, nie chcemy zwracać częściowo przydzielonej tablicy ani nie chcemy po prostu wyskoczyć z funkcji z błędem; chcemy po sobie posprzątać i zwolnić częściowo przydzieloną pamięć. W języku, który rzuca wyjątek na zły przydział, jest to dość proste - wystarczy napisać moduł obsługi wyjątków, aby zwolnić to, co już zostało przydzielone.
W C nie masz strukturalnej obsługi wyjątków; musisz sprawdzić wartość zwrotu każdego malloc
połączenia i podjąć odpowiednie działanie.
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;
}
Czy możemy to zrobić bez użycia goto
? Oczywiście, że możemy - wymaga to tylko trochę dodatkowej księgowości (i w praktyce to jest moja ścieżka). Ale jeśli szukasz miejsc, w których używanie goto
nie jest od razu oznaką złej praktyki lub projektu, jest to jedno z niewielu.