Посібник зі стилю linux дає конкретні причини для використання goto
s, які відповідають вашому прикладу:
https://www.kernel.org/doc/Documentation/process/coding-style.rst
Обґрунтуванням використання gotos є:
- безумовні твердження легше зрозуміти і слідувати
- гніздування скорочується
- помилки, не оновлюючи окремі точки виходу під час внесення змін, запобігаються
- зберігає роботу компілятора для оптимізації зайвого коду;)
Заперечення Я не повинен ділитися своєю роботою. Приклади тут трохи надумані, тому нехай будь ласка, нехай з собою.
Це добре для управління пам’яттю. Нещодавно я працював над кодом, який динамічно розподіляв пам'ять (наприклад, char *
повернута функцією). Функція, яка розглядає шлях і визначає, чи дійсний шлях, аналізуючи лексеми шляху:
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
...
some statements, some involving dynamically allocated memory
...
if ( check_this() ){
free(var1);
free(var2);
...
free(varN);
return 1;
}
...
some more stuff
...
if(something()){
if ( check_that() ){
free(var1);
free(var2);
...
free(varN);
return 1;
} else {
free(var1);
free(var2);
...
free(varN);
return 0;
}
}
token = strtok(NULL,delim);
}
free(var1);
free(var2);
...
free(varN);
return 1;
Тепер для мене наступний код набагато приємніший і простіший в обслуговуванні, якщо вам потрібно додати varNplus1
:
int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
...
some statements, some involving dynamically allocated memory
...
if ( check_this() ){
retval = 1;
goto out_free;
}
...
some more stuff
...
if(something()){
if ( check_that() ){
retval = 1;
goto out_free;
} else {
retval = 0;
goto out_free;
}
}
token = strtok(NULL,delim);
}
out_free:
free(var1);
free(var2);
...
free(varN);
return retval;
Тепер у коду були всілякі інші проблеми з ним, а саме те, що N десь вище 10, а функція - понад 450 рядків, де місцями 10 рівнів вкладеності.
Але я запропонував моєму керівнику переробити його, що я і зробив, і тепер це купа функцій, які всі короткі, і всі вони мають стиль Linux
int function(const char * param)
{
int retval = 1;
char * var1 = fcn_that_returns_dynamically_allocated_string(param);
if( var1 == NULL ){
retval = 0;
goto out;
}
if( isValid(var1) ){
retval = some_function(var1);
goto out_free;
}
if( isGood(var1) ){
retval = 0;
goto out_free;
}
out_free:
free(var1);
out:
return retval;
}
Якщо розглядати еквівалент без goto
s:
int function(const char * param)
{
int retval = 1;
char * var1 = fcn_that_returns_dynamically_allocated_string(param);
if( var1 != NULL ){
if( isValid(var1) ){
retval = some_function(var1);
} else {
if( isGood(var1) ){
retval = 0;
}
}
free(var1);
} else {
retval = 0;
}
return retval;
}
Мені, в першому випадку, для мене очевидно, що якщо повернеться перша функція NULL
, ми тут далеко і повертаємось 0
. У другому випадку я повинен прокрутити вниз, щоб побачити, що якщо міститься вся функція. Наданий перший визначає це для мене стилістично (назва " out
"), а другий робить це синтаксично. Перший все-таки більш очевидний.
Крім того, я дуже вважаю за краще мати free()
заяви в кінці функції. Це частково тому, що, з мого досвіду, free()
твердження посеред функцій погано пахнуть і вказують мені, що я повинен створити підпрограму. У цьому випадку я створив var1
свою функцію і не зміг free()
її виконати в підпрограмі, але саме тому goto out_free
стиль goto out настільки практичний.
Я думаю, що програмістів потрібно виховувати, вважаючи, що goto
це зло. Потім, коли вони достатньо зрілі, вони повинні переглядати вихідний код Linux і читати посібник зі стилю Linux.
Додам, що я використовую цей стиль дуже послідовно, у кожній функції є int retval
, out_free
label та out label. Через стилістичну узгодженість покращується читабельність.
Бонус: Перерва та продовження
Скажіть, у вас є певний час
char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
var1 = functionA(line,count);
var2 = functionB(line,count);
if( functionC(var1, var2){
count++
continue;
}
...
a bunch of statements
...
count++;
free(var1);
free(var2);
}
Є й інші помилки з цим кодом, але одне - це продовження заяви. Я хотів би переписати всю річ, але мені було поставлено завдання змінити її невеликим чином. Минуло б кілька днів, щоб переробити його таким чином, що мене задовольнило, але фактична зміна полягала приблизно в півдня. Проблема полягає в тому, що навіть якщо ми ' continue
' нам все-таки потрібно звільнитись var1
і var2
. Мені довелося додати var3
, і це змусило мене захотіти, щоб відобразити вільні () заяви.
У той час я був відносно новим стажером, але я ще раз назад дивився на вихідний код Linux, тому я поцікавився у свого керівника, чи можу я використати goto заяву. Він сказав так, і я це зробив:
char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
var1 = functionA(line,count);
var2 = functionB(line,count);
var3 = newFunction(line,count);
if( functionC(var1, var2){
goto next;
}
...
a bunch of statements
...
next:
count++;
free(var1);
free(var2);
}
Я думаю, що продовження добре в кращому випадку, але для мене вони схожі на гото з невидимою етикеткою. Те саме стосується перерв. Я б все-таки вважав за краще продовжувати або перервати, якщо, як це було у випадку, це не примушує вас відображати модифікації в декількох місцях.
І я також хочу додати, що таке використання goto next;
та next:
етикетка для мене незадовільні. Вони просто кращі, ніж дзеркальне відображення free()
та count++
твердження.
goto
майже завжди помиляються, але треба знати, коли ними добре користуватися.
Одне, що я не обговорював - це обробка помилок, на яку поширюються інші відповіді.
Продуктивність
Можна подивитися на реалізацію strtok () http://opensource.apple.com//source/Libc/Libc-167/string.subproj/strtok.c
#include <stddef.h>
#include <string.h>
char *
strtok(s, delim)
register char *s;
register const char *delim;
{
register char *spanp;
register int c, sc;
char *tok;
static char *last;
if (s == NULL && (s = last) == NULL)
return (NULL);
/*
* Skip (span) leading delimiters (s += strspn(s, delim), sort of).
*/
cont:
c = *s++;
for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
if (c == sc)
goto cont;
}
if (c == 0) { /* no non-delimiter characters */
last = NULL;
return (NULL);
}
tok = s - 1;
/*
* Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
* Note that delim must have one NUL; we stop if we see that, too.
*/
for (;;) {
c = *s++;
spanp = (char *)delim;
do {
if ((sc = *spanp++) == c) {
if (c == 0)
s = NULL;
else
s[-1] = 0;
last = s;
return (tok);
}
} while (sc != 0);
}
/* NOTREACHED */
}
Будь ласка, виправте мене, якщо я помиляюся, але я вважаю, що cont:
мітка та goto cont;
висловлювання є ефективними (вони, безумовно, не роблять код читабельнішим). Їх можна замінити на читабельний код, виконавши
while( isDelim(*s++,delim));
пропустити роздільники. Але щоб бути максимально швидким і уникати зайвих дзвінків функцій, вони роблять це так.
Я читаю статтю Дайкстра і вважаю це досить езотеричним.
google "dijkstra goto заява вважається шкідливою", оскільки мені не вистачає репутації, щоб розмістити більше 2 посилань.
Я бачив, що це наводиться як причина, щоб не використовувати goto's, і читання цього не змінило нічого, наскільки моє використання goto's.
Додаток :
Я придумав акуратне правило, думаючи про все це про продовження та перерви.
- Якщо через цикл у вас є продовження, то тіло циклу while повинно бути функцією, а продовження - оператором повернення.
- Якщо в циклі часу у вас є оператор перерви, то сам цикл while повинен бути функцією, а перерва повинна стати оператором return.
- Якщо у вас обоє, то щось може бути не так.
Це не завжди можливо через проблеми сфери, але я виявив, що це робить набагато простіше міркувати про мій код. Я помічав, що кожного разу, коли цикл перервався або продовжився, це викликало у мене погане відчуття.