Подтвердить что ты не робот

Переход от парадигмы обработки ошибок C `goto` к парадигме обработки исключений С++

Я программист C, изучающий С++. В C существует общая goto идиома, используемая для обработки ошибок и выхода из функции. Я прочитал, что обработка исключений с помощью блоков try - catch предпочтительнее в объектно-ориентированных программах, но мне трудно реализовать эту парадигму в С++.

Возьмем, например, следующую функцию в C, которая использует парадигму обработки ошибок goto:

unsigned foobar(void){
    FILE *fp = fopen("blah.txt", "r");
    if(!fp){
        goto exit_fopen;
    }

    /* the blackbox function performs various
     * operations on, and otherwise modifies,
     * the state of external data structures */
    if(blackbox()){
        goto exit_blackbox;
    }

    const size_t NUM_DATUM = 42;
    unsigned long *data = malloc(NUM_DATUM*sizeof(*data));
    if(!data){
        goto exit_data;
    }

    for(size_t i = 0; i < NUM_DATUM; i++){
        char buffer[256] = "";
        if(!fgets(buffer, sizeof(buffer), fp)){
            goto exit_read;
        }

        data[i] = strtoul(buffer, NULL, 0);
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        printf("%lu\n", data[i] + data[i + NUM_DATUM/2]);
    }

    free(data)
    /* the undo_blackbox function reverts the
     * changes made by the blackbox function */
    undo_blackbox();
    fclose(fp);

    return 0;

exit_read:
    free(data);
exit_data:
    undo_blackbox();
exit_blackbox:
    fclose(fp);
exit_fopen:
    return 1;
}

Я попытался воссоздать функцию в С++ с помощью парадигмы обработки исключений как таковой:

unsigned foobar(){
    ifstream fp ("blah.txt");
    if(!fp.is_open()){
        return 1;
    }

    try{
        // the blackbox function performs various
        // operations on, and otherwise modifies,
        // the state of external data structures
        blackbox();
    }catch(...){
        fp.close();
        return 1;
    }

    const size_t NUM_DATUM = 42;
    unsigned long *data;
    try{
        data = new unsigned long [NUM_DATUM];
    }catch(...){
        // the undo_blackbox function reverts the
        // changes made by the blackbox function
        undo_blackbox();
        fp.close();
        return 1;
    }

    for(size_t i = 0; i < NUM_DATUM; i++){
        string buffer;
        if(!getline(fp, buffer)){
            delete[] data;
            undo_blackbox();
            fp.close();
            return 1;
        }

        stringstream(buffer) >> data[i];
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }

    delete[] data;
    undo_blackbox();
    fp.close();

    return 0;
}

Я чувствую, что моя версия на С++ неправильно реализовала парадигму обработки исключений; на самом деле, версия С++ кажется еще менее читаемой и более подверженной ошибкам из-за сборки кода очистки, накапливающегося в блоках catch по мере увеличения функции.

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

4b9b3361

Ответ 1

Принцип RAII заключается в том, что вы используете тип класса для управления любым ресурсом, который необходимо очистить после использования; что очистка выполняется деструктором.

Это означает, что вы можете создать локальный менеджер RAII, который будет автоматически очищать все, что он управляет, когда он выходит из области действия, независимо от того, что из-за обычного потока программы или исключения. Никогда не должно быть необходимости в блоке catch только для очистки; только когда вам нужно обработать или сообщить об исключении.

В вашем случае у вас есть три ресурса:

  • Файл fp. ifstream уже является типом RAII, поэтому просто удалите избыточные вызовы на fp.close(), и все это хорошо.
  • выделенная память data. Используйте локальный массив, если он имеет небольшой фиксированный размер (как это есть) или std::vector, если он должен быть динамически распределен; затем избавьтесь от delete.
  • Состояние, настроенное на blackbox.

Вы можете написать свою собственную оболочку RAII для "черного ящика" malarkey:

struct blackbox_guard {
    // Set up the state on construction
    blackbox_guard()  {blackbox();}

    // Restore the state on destruction
    ~blackbox_guard() {undo_blackbox();}

    // Prevent copying per the Rule of Three
    blackbox_guard(blackbox_guard const &) = delete;
    void operator=(blackbox_guard) = delete;
};

Теперь вы можете удалить весь код обработки ошибок; Я бы указывал на неудачу через исключения (либо брошенные, либо разрешенные для распространения), а не магическое возвращаемое значение, давая:

void foobar(){
    ifstream fp ("blah.txt"); // No need to check now, the first read will fail if not open
    blackbox_guard bb;

    const size_t NUM_DATUM = 42;
    unsigned long data[NUM_DATUM];   // or vector<unsigned long> data(NUM_DATUM);

    for(size_t i = 0; i < NUM_DATUM; i++){
        string buffer;

        // You could avoid this check by setting the file to throw on error
        // fp.exceptions(ios::badbit); or something like that before the loop
        if(!getline(fp, buffer)){
             throw std::runtime_error("Failed to read"); // or whatever
        }

        stringstream(buffer) >> data[i]; // or data[i] = stoul(buffer);
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
        cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }
}

Ответ 2

Да, вы должны использовать RAII (Resource Acquisition Is Initialisation), где это возможно. Это приводит к коду, который легко читается и безопасен.

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

В вашем случае уже есть RAII, и вы просто не используете его. std::ifstream (я предполагаю, что то, на что ссылается ваш ifstream) действительно закрывается после уничтожения. Таким образом, все вызовы close() в catch могут быть безопасно опущены и произойдут автоматически - именно для чего предназначен RAII.

Для data вы должны также использовать оболочку RAII. Доступно два: std::unique_ptr<unsigned long[]> и std::vector<unsigned long>. Оба заботятся об освобождении памяти в своих деструкторах.

Наконец, для blackbox() вы можете самостоятельно создать тривиальную оболочку RAII:

struct BlackBoxer
{
  BlackBoxer()
  {
    blackbox();
  }

  ~BlackBoxer()
  {
    undo_blackbox();
  }
};

Если их переписать, ваш код станет намного проще:

unsigned foobar() {
  ifstream fp ("blah.txt");
  if(!fp.is_open()){
    return 1;
  }

  try {
    BlackBoxer b;

    const size_t NUM_DATUM = 42;
    std::vector<unsigned long> data(NUM_DATUM);
    for(size_t i = 0; i < NUM_DATUM; i++){
      string buffer;
      if(!getline(fp, buffer)){
        return 1;
      }

      stringstream(buffer) >> data[i];
    }

    for(size_t i = 0; i < NUM_DATUM/2; i++){
      cout << data[i] + data[i + NUM_DATUM/2] << endl;
    }

    return 0;
  } catch (...) {
    return 1;
  }
}

Кроме того, обратите внимание, что ваша функция использует возвращаемое значение, чтобы указать успех или неудачу. Это может быть тем, что вам нужно (если сбой является "нормальным" для этой функции), или может представлять только половину пути (если сбой также должен быть исключительным).

Если это последнее, просто измените функцию на void, избавьтесь от конструкции try - catch и сгенерируйте подходящее исключение вместо return 1;.

Наконец, даже если вы решите сохранить подход с возвращаемым значением (который является совершенно допустимым), рассмотрите возможность изменения функции для возврата bool, где true означает успех. Это более идиоматично.

Ответ 3

Позвольте мне переписать это для вас, используя идиому С++ с пояснениями, встроенными в код

// void return type, we may no guarantees about exceptions
// this function may throw
void foobar(){
   // the blackbox function performs various
   // operations on, and otherwise modifies,
   // the state of external data structures
   blackbox();

   // scope exit will cleanup blackbox no matter what happens
   // a scope exit like this one should always be used
   // immediately after the resource that it is guarding is
   // taken.
   // but if you find yourself using this in multiple places
   // wrapping blackbox in a dedicated wrapper is a good idea
   BOOST_SCOPE_EXIT[]{
       undo_blackbox();
   }BOOST_SCOPE_EXIT_END


   const size_t NUM_DATUM = 42;
   // using a vector the data will always be freed
   std::vector<unsigned long> data;
   // prevent multiple allocations by reserving what we expect to use
   data.reserve(NUM_DATUM);
   unsigned long d;
   size_t count = 0;
   // never declare things before you're just about to use them
   // doing so means paying no cost for construction and
   // destruction if something above fails
   ifstream fp ("blah.txt");
   // no need for a stringstream we can check to see if the
   // file open succeeded and if the operation succeeded
   // by just getting the truthy answer from the input operation
   while(fp >> d && count < NUM_DATUM)
   {
       // places the item at the back of the vector directly
       // this may also expand the vector but we have already
       // reserved the space so that shouldn't happen
       data.emplace_back(d);
       ++count;
   }

   for(size_t i = 0; i < NUM_DATUM/2; i++){
       cout << data[i] + data[i + NUM_DATUM/2] << endl;
   }
}

Самая мощная функция С++ - это не классы, это деструктор. Деструктор позволяет освобождать или освобождать ресурсы или обязанности при выходе из области действия. Это означает, что вам не нужно повторно писать код очистки несколько раз. Более того, поскольку только построенные объекты могут быть разрушены; если вы никогда не добираетесь до предмета и, таким образом, никогда его не строите, вы не платите никакого штрафа в уничтожении, если что-то происходит.

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

Ответ 4

В C существует общая идома goto, используемая для обработки ошибок и выхода очистка от функции. Я прочитал, что обработка исключений через блоки try-catch предпочтительнее в объектно-ориентированных программах,

Это не совсем верно для С++.

Но С++ имеет детерминированные деструкторы вместо блоков finally (которые используются, например, в Java), и что сменщик игры для кода обработки ошибок.

Я читал, что весь этот код очистки в блоках catch может быть ненужный в С++ из-за чего-то называемого RAII,

Да, на С++ вы используете "RAII". Это плохое имя для отличной концепции. Имя плохое, потому что оно делает акцент на nitialisation i (Инициализация ресурсов - инициализация). В отличие от RAII, важная вещь заключается в разрушении. Поскольку деструктор локального объекта будет выполнен в конце блока, независимо от того, что произойдет, будь то ранние возвращения или даже исключения, это идеальное место для кода, который освобождает ресурсы.

но я не знаком с этой концепцией.

Ну, в самом начале, вы можете начать с определения Википедии:

http://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization

Или вы идете прямо на сайт Bjarne Stroustrup:

http://www.stroustrup.com/bs_faq2.html#finally

Я уверен, что мы были бы более чем счастливы ответить на вопросы об отдельных аспектах идиомы или проблемах, с которыми вы сталкиваетесь:

Является ли моя реализация надлежащим образом, или есть лучший способ обработки ошибки и чистое выключение функции в С++?

Ваша реализация не, что можно было бы ожидать от хорошего кода на С++.

Вот пример использования RAII. Он использует исключения для отчетов об ошибках и деструкторы для выполнения операций очистки.

#include <fstream>
#include <stdexcept>
#include <vector>

// C or low-level functions to be wrapped:
int blackbox();
void undo_blackbox();

// just to be able to compile this example:
FILE *fp;

// The only self-made RAII class we need for this example
struct Blackbox {
    Blackbox() {
        if (!blackbox()) {
            throw std::runtime_error("blackbox failed");
        }
    }

    // Destructor performs cleanup:
    ~Blackbox() {
        undo_blackbox();
    }   
};

void foobar(void){
    // std::ifstream is an implementation of the RAII idiom,
    // because its destructor closes the file:
    std::ifstream is("blah.txt");
    if (!is) {
        throw std::runtime_error("could not open blah.txt");
    }

    Blackbox local_blackbox;

    // std::vector itself is an implementation of the RAII idiom,
    // because its destructor frees any allocated data:
    std::vector<unsigned long> data(42);

    for(size_t i = 0; i < data.size(); i++){
        char buffer[256] = "";
        if(!fgets(buffer, sizeof(buffer), fp)){
            throw std::runtime_error("fgets error");
        }

        data[i] = strtoul(buffer, NULL, 0);
    }

    for(size_t i = 0; i < (data.size()/2); i++){
        printf("%lu\n", data[i] + data[i + (data.size()/2)]);
    }

    // nothing to do here - the destructors do all the work!
}

Кстати, +1 для попытки освоить новую концепцию на новом языке. Нелегко изменить свое мышление на другом языке!:)