Comments 23
Разве тут написали что не нужен? Вроде подразумевают по контексту, что "если много использовать, то это плохо".
А можно пару-тройку примеров оправданного использования goto в C++ном коде (именно C++ном, а не чисто Сишном)? За исключением автоматически сгенерированных конечных автоматов.
Ну, выскочить из глубоких циклов на С++ тоже удобно, особенно для встраиваемых систем на микроконтроллерах, где всякие рекомендуемые "throw… catch…" редко используются в принципе.
throw+catch для выхода из вложенных циклов -- это дорого даже если исключения разрешены.
Т.е. данный пример относится к категории оптимизации, когда нужно такты сэкономить и не хочется вводить флаги для циклов, а так же у нас не получается (реально?) вынести эти самые глубокие циклы в отдельные функции (где можно делать return).
Какие еще примеры?
Про "throw+catch - дорого" - знаю, я так не делаю, но иногда именно для С++ пишут как рекомендацию, так заменять goto выход из циклов без введения флагов.
У меня нет больше примеров, я больше нигде не использую. Могу ещё придумать гипотетический случай: перенос программы с ASM в С/С++ без переделки алгоритма, "в лоб". Но это сильно экзотика.
У меня нет больше примеров, я больше нигде не использую
Ну вот в том-то и дело, что именно в C++ место для goto осталось разве что на поприще оптимизации, когда правильно работающий код уже есть, но нужно заставить его работать еще быстрее. А по сути-то "goto не нужен". Странно, что кому-то это "религией" кажется.
Я регулярно использую goto в случае, когда функция должна проверить кучу параметров на правильность и при ошибке возвращать некий код ошибки, но при этом после каждой удачной проверки выполняются какие-то действия, какие нужно откатить в случае обнаружения ошибки в дальнейшем. Т.е. получается конструкция примерно такого вида:
int Ret_Code;
if (проверка1)
{
return Error1;
}
действие1;
if (проверка2)
{
Ret_Code = Error2;
goto Err1;
}
действие2;
if (проверка3)
{
Ret_Code = Error3;
goto Err2;
}
....
return Success;
ErrN:
откатN;
...
Err2:
откат2;
Err1:
откат1;
return Ret_Code;
Как по мне, читабельность такого варианта больше, чем без goto, поскольку сами goto используются единообразным способом, без всякого там спагетти-кода.
Вы это делаете в C++? Не в теплой ламповой Сишечке, а именно в C++?
Именно. Но для микроконтроллеров без всяких осей -- и без обработки исключений, там они не очень-то уместны (и совсем неуместны, если ресурсы сильно ограничены).
И RAII там тоже ни в каком виде?
Требует использования классов, динамического выделения-освобождения памяти и всё такое. Когда ООП само по себе уместно -- скажем, нужно наследование с полиморфизмом, которые реализовывать средствами чистого Си глупо, если есть Си++, -- это нормально, но в простом низкоуровневом коде... Это чисто его раздувание без какой-либо нужды, да и читабельность снижает: разрывает фактически единую функцию между конструктором и деструктором, а последний заставляет ещё и анализировать условия завершения, чтобы определить, что именно откатывать, а что -- нет. Ну или делать кучу вспомогательных классов с той же целью. В общем, громоздко, нечитаемо и неэффективно. Ну а конструкции типа try-finally в языке нет.
Динамическое выделение/освбождение памяти -- это одна из возможных проблем, а не единственная проблема. Использование "левых" классов "размазывает" программу и сильно усложняет её восприятие -- и ради чего? Только чтоб goto не было? Это уже фанатизм, и приводит он к прямо противоположным результатам (ухудшению читабельности и лёгкости сопровождения кода вместо улучшения).
Если вы в подходе с RAII умудрились увидеть динамическое выделение памяти (как одну из проблем), то я вынужден ваши суждения отправлять прямиком в /dev/null.
Код с goto хрупок и его надежность базируется только на скрупулезности и внимательности программиста. А этим факторам доверять нельзя от слова совсем.
Вынуждены -- отправляйте. Но, замечу, что код без goto, но с классами, не менее хрупок и столь же базируется на скрупулёзности и внимательности программиста -- может, для Вас это будет открытием, но надёжность кода всегда базируется именно на них.
Ну и где вы видите размазывание вот в таком коде?
bool success = false;
if (проверка1) return Error1;
действие1;
auto fin1 = finally([&]() {
if (!success) {
откат1;
}
});
if (проверка2) return Error2;
действие2;
auto fin2 = finally([&]() {
if (!success) {
откат2;
}
});
if (проверка3) return Error3;
действие3;
auto fin3 = finally([&]() {
if (!success) {
откат3;
}
});
success = true;
return Success;
Как по мне — от одной только группировки действий и откатов уже стало проще воспринимать код. И это ещё я даже не начинал ничего упрощать...
Здесь размазывания уже нет. Правда, некоторое разбухание исходника (объявление переменных finN, тела лямбд с if'ами и обрамляющие сие дело скобки), но в разумных пределах и вполне читабельное. А можете упростить? Интересно посмотреть, что получится.
Из недостатков -- выделение дополнительной памяти. Да, знаю, не динамической, а в стеке -- но, тем не менее. Когда у тебя всего ОЗУ, скажем, 6 Кбайт на всё про всё, это может быть критичным (а может и не быть -- от задачи зависит). Плюс, некоторое снижением производительности, но это даже на слабых МК крайне редко проблемой бывает (а где бывает, лично я бы просто перешёл на ассемблер -- на нём я всяко напишу и более быстрый, и более компактный код, чем самый наилучший компилятор).
А где он нужен?
60 антипаттернов для С++ программиста, часть 7 (совет 31 — 35)