Чому гото небезпечно?
goto
не викликає нестабільність сама по собі. Незважаючи на близько 100 000 goto
с, ядро Linux все ще є моделлю стабільності.
goto
сам по собі не повинен викликати вразливості безпеки. Однак у деяких мовах змішування його з блоками управління try
/ catch
виключення може призвести до вразливості, як це пояснено в цій рекомендації CERT . Основні компілятори C ++ позначають і запобігають таким помилкам, але, на жаль, старіші або більш екзотичні компілятори цього не роблять.
goto
викликає нечитабельний і нездійсненний код. Це також називається кодом спагетті , оскільки, як і в тарілці зі спагетті, дуже важко стежити за потоком контролю, коли є занадто багато гото.
Навіть якщо вам вдасться уникнути коду спагетті, і якщо ви використовуєте лише кілька готів, вони все одно полегшують помилки, як і витік ресурсів:
- Код за допомогою структурного програмування, з чіткими вкладеними блоками та петлями або комутаторами, легко дотримуватися; його потік управління дуже передбачуваний. Тому легше забезпечити повагу інваріантів.
- З
goto
твердженням ви перериваєте цей прямий потік і ламаєте очікування. Наприклад, ви можете не помітити, що вам все-таки потрібно звільнити ресурси.
- Багато
goto
в різних місцях можуть відправити вас до однієї цілі переходу. Тож не очевидно знати напевно, у якому стані ви знаходитесь, коли доїдете до цього місця. Отже, ризик зробити помилкові / необгрунтовані припущення є досить великим.
Додаткова інформація та цитати:
C надає нескінченно зловживану goto
заяву та мітки, на які слід відгалужуватись. Формально goto
це ніколи не потрібно, і на практиці майже завжди легко писати код без нього. (...)
Тим не менш, ми запропонуємо кілька ситуацій, коли гото може знайти місце. Найбільш поширене використання - це відмовитися від обробки в деяких глибоко вкладених структурах, таких як прорив відразу двох циклів. (...)
Хоча ми не догматичні щодо цього питання, але здається, що goto-твердження слід застосовувати досить щадно, якщо вони взагалі є .
Коли можна було б використовувати?
Як і K&R, я не догматично ставився до готів. Я визнаю, що бувають ситуації, коли гото може полегшити життя.
Як правило, в C goto дозволяє виходити з багаторівневого циклу або обробляти помилки, вимагаючи досягти відповідної точки виходу, що звільняє / розблоковує всі ресурси, що були виділені до цього часу (неодноразовий розподіл у послідовності означає кілька міток). Ця стаття кількісно визначає різні способи використання goto в ядрі Linux.
Особисто я вважаю за краще уникати цього і за 10 років С я використав максимум 10 готів. Я вважаю за краще використовувати вкладені if
s, які, на мою думку, є більш читабельними. Коли це призведе до надто глибокого вкладення, я б вирішив або розкласти свою функцію на менші частини, або використовувати бульний індикатор у каскаді. Сьогоднішні оптимізуючі компілятори досить розумні, щоб генерувати майже той самий код, ніж той самий код goto
.
Використання goto значно залежить від мови:
У C ++, правильне використання RAII змушує компілятор автоматично знищувати об'єкти, що виходять за межі сфери, так що ресурси / блокування будуть очищені в будь-якому випадку, і більше немає потреби в goto.
У Java немає необхідності Гото (див автора цитати в Java вище , і цей чудовий Stack Overflow відповідь ): збирач сміття , який очищає безлад, break
, continue
і try
/ catch
обробка винятків охоплює всі той випадок , коли goto
може бути корисними, але в більш безпечному і краще манера. Популярність Java доводить, що готові твердження можна уникнути сучасною мовою.
Масштабування знаменитого вразливості SSL перебуває у відмові
Важлива відмова: з огляду на жорстоку дискусію в коментарях, я хочу уточнити, що я не претендую на те, що заява goto є єдиною причиною цієї помилки. Я не претендую, що без Goto не було б жодної помилки. Я просто хочу показати, що гото може бути причетним до серйозної помилки.
Я не знаю, скільки серйозних помилок пов’язано goto
з історією програмування: деталі часто не повідомляються. Однак була відома помилка Apple SSL, яка послабила безпеку iOS. Заява, яка призвела до цієї помилки, була неправильною goto
заявою.
Деякі стверджують, що першопричиною помилки була не сама заява goto, а неправильна копія / вставка, оманливий відступ, відсутні фігурні дужки навколо умовного блоку чи, можливо, робочі звички розробника. Я не можу ані підтвердити жодного з них: всі ці аргументи є ймовірними гіпотезами та інтерпретацією. Ніхто насправді не знає. ( тим часом, гіпотеза про злиття, що пішла не так, як хтось запропонував у коментарях, здається, дуже хорошим кандидатом з огляду на деякі інші невідповідності відступу в тій же функції ).
Єдиний об'єктивний факт полягає в тому, що дублювання goto
призвело до того, щоб передчасно вийти з функції. Дивлячись на код, єдиним іншим твердженням, яке могло викликати той же ефект, було б повернення.
Помилка функціонує SSLEncodeSignedServerKeyExchange()
в цьому файлі :
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
goto fail;
if ((err =...) !=0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
if (...)
goto fail;
... // Do some cryptographic operations here
fail:
... // Free resources to process error
Дійсно фігурні дужки навколо умовного блоку могли запобігти помилці:
це призвело б або до синтаксичної помилки під час компіляції (і, отже, виправлення), або до зайвого нешкідливого goto. До речі, GCC 6 зможе помітити ці помилки завдяки додатковому попередженню для виявлення непослідовних відступів.
Але, по-перше, усіх цих готів можна було б уникнути більш структурованим кодом. Тож гото принаймні опосередковано є причиною цієї помилки. Існують щонайменше два різні способи, які могли б цього уникнути:
Підхід 1: якщо пункт або вкладено if
s
Замість того, щоб послідовно перевіряти безліч умов на помилку, і кожного разу надсилаючи fail
ярлики у разі проблеми, можна було б if
обрати для виконання криптографічних операцій у висловленні, що зробило б це, лише якщо не було неправильної попередньої умови:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
Підхід 2: використовувати акумулятор помилок
Цей підхід заснований на тому, що майже всі тут твердження викликають якусь функцію для встановлення err
коду помилки, а решту коду виконують лише, якщо err
було 0 (тобто функція виконується без помилок). Приємною безпечною та читаною альтернативою є:
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
Тут немає жодного гото: немає ризику швидко перескочити до точки виходу з ладу. І візуально було б легко помітити нерівну лінію або забуту ok &&
.
Ця конструкція є більш компактною. Він заснований на тому, що в C друга частина логічного і ( &&
) оцінюється лише за умови, що перша частина є істинною. Насправді асемблер, що генерується оптимізуючим компілятором, майже еквівалентний вихідному коду з gotos: Оптимізатор дуже добре виявляє ланцюг умов і генерує код, який при першому ненульовому поверненому значенні стрибає до кінця ( онлайн-доказ ).
Можна навіть передбачити перевірку узгодженості в кінці функції, яка могла б на етапі тестування виявити невідповідність між прапором ОК та кодом помилки.
assert( (ok==false && err!=0) || (ok==true && err==0) );
Помилки, такі як ==0
ненароком замінені !=0
помилки або логічні з'єднувачі, легко виявлятимуться на етапі налагодження.
Як було сказано: Я не претендую на те, що альтернативні конструкції уникали б жодної помилки. Я просто хочу сказати, що вони могли ускладнити виникнення помилок.