2012-01-31 3 views
1

Хорошо, вопрос века :)Можно ли использовать goto в следующей ситуации?

Прежде чем вы что-нибудь скажете или подумаете, позвольте мне сказать вам, что я прочитал пару подобных вопросов по этой теме, но я не нашел ясного решения для своих проблема. Мой случай специфичен и типичен для системных программистов, я думаю.

У меня такая ситуация очень часто. Я ненавижу gotos, не знаю, почему, возможно потому, что все кричат, что это плохо. Но до сих пор я не нашел лучшего решения для моего конкретного сценария, и способ, которым я это делаю в настоящее время, может быть более уродливым, чем использование goto.

Вот мой случай: я использую C++ (Visual C++) для разработки приложений Windows, и довольно часто я использую кучу API в своих подпрограммах. Предположим, что следующая ситуация:

int MyMemberFunction() 
{ 
    // Some code... // 

    if (!SomeApi()) 
    { 
     // Cleanup code... // 

     return -1; 
    } 

    // Some code... // 

    if (!SomeOtherApi()) 
    { 
     // Cleanup code... // 

     return -2; 
    } 

    // Some more code... // 

    if (!AnotherApi()) 
    { 
     // Cleanup code... // 

     return -3; 
    } 

    // More code here... // 

    return 0; // Success 
} 

Таким образом, после каждого Апи я должен проверить, если это удалось, и прервать свою функцию, если это не так. Для этого я использую целую кучу // Cleanup code... //, часто почти дублируемую, а затем инструкцию return. Функция выполняет, скажем, 10 задач (например, использует 10 Apis), и если задача №6 терпит неудачу, я должен очистить ресурсы, созданные предыдущими задачами. Обратите внимание, что очистка должна выполняться самой функцией, поэтому обработка исключений не может быть использована. Кроме того, я не вижу, как много разговорный RAII может помочь мне в этой ситуации.

Единственный способ, о котором я думал, - использовать goto для перехода от всех таких случаев отказа к одной метке очистки, помещенной в конце функции.

Есть ли лучший способ сделать это? Будет ли использование goto считаться плохой практикой в ​​такой ситуации? Что делать тогда? Такая ситуация очень типична для меня (и для системных программистов, подобных мне, я считаю).

P.S .: Ресурсы, которые необходимо очищать, имеют разные типы. Там может быть распределение памяти, различные ручки системных объектов, которые необходимо закрыть и т.д.

UPDATE:

Я думаю, что люди до сих пор не поняли, что я хотел (вероятно, я объясняю плохо). Я думал, что псевдокода должно быть достаточно, но вот пример:

  1. Открываю два файла с помощью CreateFile. Если этот шаг завершился неудачно: я должен очистить дескрипторы уже открытых файлов, если они есть. Я позже прочитаю часть одного файла и напишу в другой.

  2. Я использую SetFilePointer для указания указателя на чтение в первом файле. Если этот шаг завершится неудачно: мне нужно закрыть дескрипторы, открытые предыдущим шагом.

  3. Я использую GetFileSize для получения размера целевого файла. Если api не удается, или размер файла ненормален, я должен выполнить очистку: то же, что и на предыдущем шаге.

  4. Я выделяю буфер указанного размера для чтения из первого файла. Если сбой памяти не удался, я снова должен закрыть дескрипторы файлов.

  5. Мне нужно использовать ReadFile для чтения из первого файла. Если это не удается, я должен: освободить буферную память и закрыть дескрипторы файлов.

  6. Я использую SetFilePointer, чтобы указать указатель записи во втором файле. Если это не удается, необходимо выполнить такую ​​же очистку.

  7. Я должен использовать WriteFile для записи во второй файл. Если это не удается, бла-бла-бла ...

Кроме того, предположим, что я охраняю эту функцию с критической секцией, и после того, как я называю EnterCriticalSection в начале функции, я должен позвонить LeaveCriticalSection перед каждым return заявление.

Обратите внимание, что это очень упрощенный пример. Там может быть больше ресурсов, и нужно сделать больше очистки - в основном то же самое, но иногда немного другое, основанное на том, какой шаг потерпел неудачу. Но давайте поговорим в этом примере: как я могу использовать RAII здесь?

+3

[Как плохо это может быть?] (Http://xkcd.com/292/) (Но серьезно, это неплохо, если он производит * более читаемый код *. Это вызов кишки, который вам нужно сделать.) –

+1

Является ли код очистки общим, или код очистки зависит от того, как далеко вы попадете в эту функцию? – Corbin

+0

Один из способов избежать goto - бросить int, а затем поймать его. – QuentinUK

ответ

4

RAII будет работать для этого, если ресурсы, созданные предыдущими задачами, поддерживаются в виде классов/объектов, которые очищаются после себя. Вы упомянули дескрипторы памяти и системных объектов, поэтому давайте использовать их в качестве отправной точки.

// non RAII code: 
int MyMemberFunction() { 
    FILE *a = fopen("Something", "r"); 

    if (!task1()) { 
     fclose(a); 
     return -1; 
    } 

    char *x = new char[128]; 

    if (!task2()) { 
     delete [] x; 
     fclose(a); 
     return -2; 
    } 
} 

RAII на основе кода:

int MyMemberFunction() { 
    std::ifstream a("Something"); 
    if (!task1()) 
     return -1; // a closed automatically when it goes out of scope 

    std::vector<char> x(128); 

    if (!task2()) 
     return -2; // a closed, x released when they go out of scope 
    return 0; // again, a closed, x released when they go out of scope 
} 

отметить также, что, если вы обычно ожидаете вещи работать, вы можете написать код, чтобы изобразить, что немного более тесно:

int MyMemberFunction() { 
    bool one, two; 

    if ((one=task1()) && (two=task2())) 
     return 0; 

    // If there was an error, figure out/return the correct error code. 
    if (!one) 
     return -1; 
    return -2; 
} 

Редактирование: Хотя это довольно необычно, если вам действительно нужно использовать c-style I/O, вы все равно можете перевести его в класс C++, смутно похожий на iostream:

class stream { 
    FILE *file; 
public: 
    stream(char const &filename) : file(fopen(filename, "r")) {} 
    ~stream() { fclose(file); } 
}; 

Это очевидно упрощено (много), но общая идея работает отлично. Существует менее очевидный, но в целом отличный подход: iostream фактически использует класс буфера, используя underflow для чтения и overflow для записи (опять же, упрощая, хотя и не так много в этот раз). Не сложно написать класс буфера, который использует FILE * для обработки чтения/записи. Большая часть задействованного кода на самом деле немного больше, чем довольно тонкий слой перевода, чтобы обеспечить правильные имена функций, изменить параметры на нужные типы и заказы и т. Д.

В случае памяти у вас есть два разных подхода. Одна из них, как это, пишет вектор-подобный класс свой собственный, который действует исключительно в качестве обертки вокруг того, управление памятью, необходимо использовать (new/delete, malloc/free и т.д.)

Другой подход заключается в наблюдении, что std::vector имеет параметр Allocator, поэтому на самом деле это уже просто оболочка, и вы можете определить, как она получает/освобождает память.Если, например, вы действительно нуждались в том, чтобы его исходный код был malloc и free, было бы довольно легко написать класс Allocator, который их использовал. Таким образом, большая часть вашего кода будет соответствовать нормальным соглашениям на C++, используя std::vector, как и все остальное (и все еще поддерживает RAII, как указано выше). В то же время вы получаете полный контроль над реализацией управления памятью, поэтому можете использовать new/delete, malloc/free или что-то непосредственно из ОС, если необходимо (например, LocalAlloc/LocalFree на Windows).

+0

Ваш код на основе RAII просто использует другой код. Что делать, если мне нужно использовать динамическое выделение памяти с помощью 'new' вместо' std :: vector' и 'fopen' вместо' std :: ifstream'? Это самый важный момент здесь. –

+0

@TX_: см. Править в ответ. –

+0

Менеджмент распределения памяти/управления освобождением здесь является меньшей проблемой. В основном я должен очистить некоторые ресурсы ОС, что означает закрытие дескрипторов, удаление объектов и т. Д. Вот почему я использовал термин «системное программирование». Пожалуйста, просмотрите редактирование моего основного сообщения. В приведенном примере он должен освободить память, закрыть дескрипторы файлов и оставить критический раздел. И это действительно упрощенная ситуация. Как можно использовать RAII здесь? –

7

Использование goto не требуется, оно подвержено ошибкам, что приводит к избыточному и довольно небезопасному коду.

Используйте RAII, и вам не обязательно использовать goto. RAII до smart pointers идеально подходит для вашего сценария.

Вы гарантируете, что все ваши ресурсы в области управления являются RAII (либо используйте интеллектуальные указатели, либо собственные классы управления ресурсами для них), всякий раз, когда возникает условие ошибки, все, что вам нужно сделать, это возврат, а RAII магически освобождает ваши ресурсы неявно ,

+0

Дайте мне пример для псевдокода, который я разместил. –

+0

@TX_, вам нужно предоставить дополнительную информацию о том, что такое код очистки. Возможно, вы захотите создать свой собственный класс RAII. –

+0

В зависимости от того, какое «системное программирование» он делает, введение RAII может быть сомнительным по соображениям производительности (возможно, нет, но следует учитывать, на мой взгляд). – Corbin

Смежные вопросы