Skip to content

Latest commit

 

History

History
159 lines (128 loc) · 7.02 KB

function-try-catch.md

File metadata and controls

159 lines (128 loc) · 7.02 KB

Function-try-block

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

// Стандартный способ
void f() {
    try {
        may_throw();
    } catch (...) {
        handle_error(); 
    }
}

// Альтернативный синтаксис 
void f() try {
    may_throw();
} catch (...) {
    handle_error();
}

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

struct ThrowInCtor {
    ThrowInCtor() {
        throw std::runtime_error("err1");
    }
};


struct TryStruct1 {
    TryStruct1() try {

    } catch (const std::exception& e) {
        // будет поймано исключение из конструктора `c`
        std::cout << e.what() << "\n";
    }
    ThrowInCtor c;
};

struct TryStruct2 {
    TryStruct2() {
        try {

        } catch (const std::exception& e) {
            // исключение не будет поймано, поскольку тело конструктора
            // исполняется после инициализации полей
            std::cout << e.what() << "\n";
        }
    }
    ThrowInCtor c;
};

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

Потому можно иногда встретить такие страшные нагромождения

struct S {

    S(...) try :
        a(...),
        b(...) {
       try {
           init();
       } catch (const std::exception& e) {
           log(e);
           try_repair();
       }    
    } catch (const std::exeption& e) {
        // не получилось починить или неисправимая ошибка в полях
        log(e);
        // implicit rethrow
    }

    A a;
    B b;
}; 

Ну хорошо. А как насчет деструкторов? Ведь из деструкторов крайне нежелательно выкидывать исключения, и возможность красиво и просто поставить catch, который бы гарантированно перехватил все, весьма недурна.

struct DctorThrowTry {
    ~DctorThrowTry() try {
        throw std::runtime_error("err");
    } catch (const std::exception& e) {
        std::cout << e.what() << "\n";
    }
};

Выглядит неплохо. Но у нас C++, так что это не работает!

Кто-то очень доброжелательный решил, что в случае с деструкторами поведение по умолчанию должно быть таким же как и с конструкторами. То есть catch блок деструктора неявно прокидывает исключение дальше. И привет всем возможным проблемам с исключениями из деструкторов, в том числе нарушению неявного noexcept(true).

Однако, в отличие от конструкторов, для деструкторов добавили возможность подавить неявное пробрасывание пойманного исключения. Для этого нужно всего лишь... добавить return!

struct DctorThrowTry {
    ~DctorThrowTry() try {
        throw std::runtime_error("err");
    } catch (const std::exception& e) {
        std::cout << e.what() << "\n";
        return; // исключение не будет перевыброшено!
    }
};

Удивительно, но таким образом в C++ есть случай, в котором return последней командой в void-функции меняет ее поведение.

Также нужно добавить, что в catch блоке деструкторов и конструкторов нельзя обращаться к нестатическим полям и методам класса — будет неопределенное поведение. По понятным причинам. В момент входа в catch блок они все уже мертвы.

struct S {
    A a;
    B b;

    S() try {
        ...
    } catch (...) {
        do_something(a); // UB!
    }

    ~S() try {
        ...
    } catch (...) {
        do_something(b); // UB!
        return;
    } 
};

// Но при этом

bool fun(T1 a, T2 b) try {
    ...
    return true;
} catch (...) {
    // важно: этот блок не ловит исключения, возникающие при инициализации a и b
    do_something(a); // Ok!
    return false;
}

Итого

  1. Для обычных функций и main() с помощью альтернативного синтаксиса можно удобно и красиво перехватывать все исключения, которые могли бы вылететь. И поведение по умолчанию — именно перехват. Дальше не летит.
  2. Для конструкторов можно ловить исключения из конструкторов полей, обрабатывать их (печатать в лог), но подавить нельзя. Либо кидаете свое новое исключение, либо пойманное неявно будет проброшено дальше.
  3. Для деструкторов также будет неявный проброс, но его можно подавить, добавив return.

Если что, в Rust нет исключений (но есть очень похожие паники). Живите с этим.

Полезные ссылки

  1. https://en.cppreference.com/w/cpp/language/function-try-block
  2. https://mariusbancila.ro/blog/2019/03/13/little-known-cpp-function-try-block/