У меня есть вопрос об использовании оператора goto в C ++. Я понимаю, что эта тема противоречива, и я не заинтересован в каких-либо широких советах или аргументах (я обычно отказываюсь от использования goto
). Скорее, у меня есть конкретная ситуация, и я хочу понять, является ли мое решение, использующее оператор goto, хорошим или нет. Я бы не назвал себя новичком в C ++, но и не отнесу себя к программистам профессионального уровня. Часть кода, сгенерировавшая мой вопрос, вращается в бесконечном цикле после запуска. Общий поток потока в псевдокоде выглядит следующим образом:
void ControlLoop::main_loop()
{
InitializeAndCheckHardware(pHardware) //pHardware is a pointer given from outside
//The main loop
while (m_bIsRunning)
{
simulated_time += time_increment; //this will probably be += 0.001 seconds
ReadSensorData();
if (data_is_bad) {
m_bIsRunning = false;
goto loop_end;
}
ApplyFilterToData();
ComputeControllerOutput();
SendOutputToHardware();
ProcessPendingEvents();
while ( GetWallClockTime() < simulated_time ) {}
if ( end_condition_is_satisified ) m_bIsRunning = false;
}
loop_end:
DeInitializeHardware(pHardware);
}
Указатель pHardware передается извне объекта ControlLoop и имеет полиморфный тип, поэтому для меня нет особого смысла использовать RAII и создавать и разрушать сам аппаратный интерфейс внутри main_loop. Я полагаю, что pHardware мог бы создать временный объект, представляющий своего рода «сессию» или «использование» оборудования, которое можно было автоматически очистить при выходе из main_loop, но я не уверен, сделает ли эта идея более понятной кому-либо иначе каково мое намерение. Выход из цикла будет только три: первый — считывание плохих данных с внешнего оборудования; во-вторых, если ProcessPendingEvents () указывает на отмену, инициированную пользователем, что просто приводит к тому, что m_bIsRunning становится ложным; и последний, если конечное условие выполняется в нижней части цикла. Следует также отметить, что main_loop можно запускать и завершать несколько раз в течение срока службы объекта ControlLoop, поэтому он должен завершиться чисто с помощью m_bIsRunning = false
после этого.
Кроме того, я понимаю, что мог бы использовать ключевое слово break здесь, но большинство этих вызовов функций псевдокода внутри main_loop на самом деле не инкапсулированы как функции, просто потому, что им нужно было бы либо иметь много аргументов, либо им всем был бы необходим доступ к переменным-членам. На мой взгляд, оба этих случая были бы более запутанными, чем просто оставить main_loop более длинной функцией, а из-за длины большого цикла while оператор вроде goto loop_end
кажется, читается понятнее для меня.
Теперь вопрос: не доставит ли вам этого решения неудобства, если вы напишете его в своем собственном коде? Мне кажется, что это немного неправильно, но я никогда раньше не использовал выражение goto в коде C ++ — отсюда мой запрос о помощи от экспертов. Существуют ли другие основные идеи, которые мне не хватает, которые могли бы сделать этот код более понятным?
Благодарю.
С вашим единственным условием, которое вызывает преждевременное прерывание цикла, я бы просто использовал break
, Нет необходимости goto
это то что break
для.
Однако если любой из этих вызовов функций может вызвать исключение или если вам в конечном итоге понадобится несколько break
s Я бы предпочел контейнер в стиле RAII, это именно то, для чего нужны деструкторы. Вы всегда выполняете вызов DeInitializeHardware
, так…
// todo: add error checking if needed
class HardwareWrapper {
public:
HardwareWrapper(Hardware *pH)
: _pHardware(pH) {
InitializeAndCheckHardware(_pHardware);
}
~HardwareWrapper() {
DeInitializeHardware(_pHardware);
}
const Hardware *getHardware() const {
return _pHardware;
}
const Hardware *operator->() const {
return _pHardware;
}
const Hardware& operator*() const {
return *_pHardware;
}
private:
Hardware *_pHardware;
// if you don't want to allow copies...
HardwareWrapper(const HardwareWrapper &other);
HardwareWrapper& operator=(const HardwareWrapper &other);
}
// ...
void ControlLoop::main_loop()
{
HardwareWrapper hw(pHardware);
// code
}
Теперь, что бы ни случилось, вы всегда будете звонить DeInitializeHardware
когда эта функция вернется.
Избегать использования goto
это довольно солидная вещь в объектно-ориентированной разработке в целом.
В вашем случае, почему бы просто не использовать break
выйти из цикла?
while (true)
{
if (condition_is_met)
{
// cleanup
break;
}
}
Что касается вашего вопроса: ваше использование goto
сделало бы меня неудобным. Единственная причина, по которой break
менее читабельным является ваш доступ к тому, чтобы не быть сильным разработчиком C ++. Любой опытный разработчик языка C-like, break
оба будут читать лучше, а также обеспечат более чистое решение, чем goto
,
В частности, я просто не согласен с тем, что
if (something)
{
goto loop_end;
}
более читабельно, чем
if (something)
{
break;
}
который буквально говорит то же самое со встроенным синтаксисом.
ОБНОВИТЬ
Если ваша основная проблема заключается в том, что цикл while слишком длинный, вы должны стремиться сделать его короче, C ++ — это ОО-язык, а ОО — для разделения вещей на мелкие части и компоненты, даже в целом не-ОО-языке, который мы обычно по-прежнему считаем должен разбить метод / цикл на маленький и сделать его коротким для чтения. Если в цикле 300 строк, независимо от того, что break / goto действительно не экономит ваше время, не так ли?
ОБНОВИТЬ
Я не против идти к но я не буду использовать это здесь, как вы, я предпочитаю просто использовать перерыв, как правило, разработчику, что он видел перерыв там он знает, что означает перейти к концу времени, и с этим m_bIsRunning = false он может легко понять, что это на самом деле выйти из цикла в течение нескольких секунд. да идти к может сэкономить время на секунды, чтобы понять это, но это также может заставить людей нервничать по поводу вашего кода.
Я могу себе представить, что я использую goto, чтобы выйти из двухуровневого цикла:
while(running)
{
...
while(runnning2)
{
if(bad_data)
{
goto loop_end;
}
}
...
}
loop_end:
Вместо того, чтобы использовать goto
, вы должны использовать break;
чтобы избежать петель.
Есть несколько альтернатив goto
: break
, continue
а также return
в зависимости от ситуации.
Тем не менее, вы должны иметь в виду, что оба break
а также continue
ограничены в том, что они влияют только на самый внутренний цикл. return
с другой стороны, это ограничение не затрагивается.
В общем, если вы используете goto
в выход конкретный объем, то вы можете рефакторинг, используя другую функцию и return
утверждение вместо. Вероятно, это облегчит чтение кода в качестве бонуса:
// Original
void foo() {
DoSetup();
while (...) {
for (;;) {
if () {
goto X;
}
}
}
label X: DoTearDown();
}
// Refactored
void foo_in() {
while (...) {
for (;;) {
if () {
return;
}
}
}
}
void foo() {
DoSetup();
foo_in();
DoTearDown();
}
Примечание: если ваше тело функции не может удобно разместиться на экране, вы делаете это неправильно.
Goto не является хорошей практикой для выхода из цикла, когда break
это вариант.
Кроме того, в сложных подпрограммах хорошо иметь только одну логику выхода (с очисткой), размещенную в конце. Goto иногда используется для перехода к логике возврата.
Пример из блочного драйвера QEMU vmdk:
static int vmdk_open(BlockDriverState *bs, int flags)
{
int ret;
BDRVVmdkState *s = bs->opaque;
if (vmdk_open_sparse(bs, bs->file, flags) == 0) {
s->desc_offset = 0x200;
} else {
ret = vmdk_open_desc_file(bs, flags, 0);
if (ret) {
goto fail;
}
}
/* try to open parent images, if exist */
ret = vmdk_parent_open(bs);
if (ret) {
goto fail;
}
s->parent_cid = vmdk_read_cid(bs, 1);
qemu_co_mutex_init(&s->lock);
/* Disable migration when VMDK images are used */
error_set(&s->migration_blocker,
QERR_BLOCK_FORMAT_FEATURE_NOT_SUPPORTED,
"vmdk", bs->device_name, "live migration");
migrate_add_blocker(s->migration_blocker);
return 0;
fail:
vmdk_free_extents(bs);
return ret;
}
Я вижу множество людей, предлагающих break
вместо goto
, Но break
нет «лучше» (или «хуже»), чем goto
,
Инквизиция против goto
эффективно началось с Дейкстры Бумага «Считать Вредным» еще в 1968 году, когда код спагетти был правилом, и такие вещи, как блочная структура if
а также while
заявления все еще рассматривались передовой. У ALGOL 60 они были, но по сути это был исследовательский язык, используемый учеными (см. ML сегодня); Фортран, один из доминирующих языков в то время, не получал их еще 9 лет!
Основные моменты в работе Дейкстры:
goto
во всех его различных формах, то возможно знать вещи о возможных состояниях переменных в каждой лексической позиции в программе. В частности, в конце while
цикл, вы знаете, что условие этого цикла должно быть ложным. Это полезно для отладки. (Дейкстра не совсем так говорит, но вы можете сделать вывод.)break
, как goto
(и рано return
s и исключения …), уменьшает (1) и исключает (2). Конечно, используя break
часто позволяет избежать написания запутанной логики для while
условие, что дает вам чистый выигрыш в понимании — и точно так же относится к goto
,