sunLoadingImage
whowedImag
decoration left 1
decoration left 2
transhome
transprojects
transgallery
transarticles
decoration rigth
English
Українська
Show/Hide search bar
black cat logo variable logo
[2 Лис 2011]

Винятки в С++

Основи

Для обробки помилок в програмі можна використовувати систему кодів помилок, що мають повертаютися методами, чи змінювати глобальні змінні-стани, чи... В будь-якому випадку код програми збільшується, і біля основного коду програми назбирується код підтримки - перевірки на помилки та реакція на помилки. Для розділення основного коду та обробки помилок можна використовувати винятки. Винятки виникають у виключних помилкових сутуаціях під час виконання програми, наприклад, при доступі до неіснуючого елемента контейнера std::vector.

Для роботи з винятками використовують три ключові слова: try, catch і throw:
  • try - оголошує блок коду в якому буде вестися контроль за винятками. Якщо в блоці відбувся виняток, то виняток пересилається блокам catch, які йдуть після блоку try.
  • catch - оголошує блок коду, що відповідає за обробку винятків. Може бути декілька послідовних обробників для різних типів винятків.
  • throw - створює виняток. Тип винятка буде відповідати виразу після слова throw. Наприклад throw 5; створює виняток з типом int, а throw "help"; створює виняток з типом const char *.
  • try{ // блок контролю за винятками
        throw "Exception message"; // створити виняток з типом const char *
    }
    catch( int & e ){ // обробка винятків з типом int
        std::cout << e;// обробка винятку, наприклад, повідомленя у консоль
    }
    catch( const char* & e ){ // обробка винятків з типом const char *
        std::cout << e;// обробка винятку, наприклад, повідомленя у консоль
    }
    catch(...){ // обробка винятків з будь-яким типом
        std::cout << "Unknown exception";
    }

    І робочий приклад. Наприклад, доступ до неіснуючого елемента вектора:

    std::vector A; // пустий масив
    try{
        A.at(0); // доступ до неіснуючого елемента
        // A[0] не згенерує винятку, бо при доступі
        // через [] виняток не генеруються
    }
    catch(std::exception & e){ // базовий тип винятку з стандартної бібліотеки С++
        std::cout<< e.what(); // вивід повідомлення: vector::_M_range_check
    }

    При винятку конроль переходить від рядка в якому виник виняток до підходящого обробника catch. Якщо виняток стався у функції, а обробник винятків є поза тілом функції, то відбувається вихід з функції до обробника без подальшого виконання коду тіла функції. При цьому відбувається розвернення стека назад. Усі автоматичні змінні (які створенні в стеку) автоматично видаляються. Пам'ять, яка виділена динамічно, сама не видаляється. Щоб видалити динамічно виділену память, потрібно використовувати розумні вказівники. Наприклад:

    void func(){ // функція, яка створює виняток
        classCat cat1; // обєкт на стеку, автоматичне видалення
        Cat *cat2 = new Cat(); // пам'ять виділена динамічно, не видалиться автоматично
        throw "test"; // виняток зумовлює перехід до зовнішнього обробника винятків
        Cat *cat3 = new Cat(); // не буде виконано
    }

    int main(){
        try{
            func();
        }
        catch(...){} // обробка винятку з функції func()
    }
    Виняток поза блоком try: terminate()

    Якщо виняток створений поза блоком try {} або не обробився жодним блоком catch {}, то програма припиняє виконання. Наприклад:

    std::vector A; // пустий масив
    A.at(0); // виняток поза блоком try

    В консоль буде виведено наступну помилку: terminate called after throwing an instance of 'std::out_of_range'. Помилка означає, що виняток не був обробленим і через це викликалася функція terminate(). Функція terminate() завершує виконання програми викликом функції abort(). Функцію terminate() можна замінити з допомогою функції set_terminate(). Аргументом до set_terminate є вказівник на функцію. Наприклад, для завершення програми без помилки можна замінити виклик abort() на exit(): #include <exception>
    void customTerminate(){ // нова функція для неочікуваного завершення роботи програми
        std::cout << "Unhandled exception!!!" << std::endl;
        exit(0); // завершення роботи програми без помилки
    }
    int main(){
        std::set_terminate(customTerminate); // заміна функції terminate()
        throw "Test";
    }

    Ієрархія стандартних винятків

    В стандартній бібліотеці шаблонів є наступна ієрархія винятків:

    exception // базовий тип винятка
        bad_alloc // виняток при помилці виділення пам'яті
        bad_cast // виняток при неможливості перетворення типу через dynamic_cast
        bad_typeid // для винятків функції typeid
        bad_exception // неочікуваний виняток в С++ програмі
        logic_failure // винятки, які можуть бути знайдені до виконання програми
            domain_error // математичні винятки, напр, корінь з відємного
            invalid_argument // недозволені аргументи
            lenght_error // помилки при зміні розміру string, vector, і тп.
            out_of_range // доступ до неіснуючого елемента контейнера
        runtime_error // для винятків які можуть виникти тільки під час виконання програми
            overflow_error, underflow_error // математичні помилки переповнення
            range_error // помилки розрахунку меж при внутрішніх обрахунках
    Ієрархія винятків створена для можливості логічно структурувати винятки і їх обробку. Виняток типу out_of_range є дочірнім до винятку logic_failure, а logic_failure в свою чергу є дочірнім до базового винятку exception. Наприклад, наступний код окремо обробляє винятки типу out_of_range, окремо обробляє інші винятки стандартної бібліотеки шаблоків, і окремо винятки усіх інших типів:
    try{
        // код який може генерувати різні винятки
    }
    catch(std::out_of_range & e){
        // обробка винятків - доступ до неіснуючих елементів контейнера
    }
    catch(std::exception & e){
        // обробка винятків - будь-які винятки зі стандартної бібліотеки шаблонів
    }
    catch(...){
        // обробка усіх інших можливих винятків
    }
    Можна створити свою ієрархію винятків, використавши виняток exception як базовий і замінивши частину базової функціональності. Наприклад, виняток graphicsException буде членом групи винятків exception. Якщо не буде блоку catch, що обробляє винятки graphicsException, то виняток graphicsException буде оброблений блоком catch, що обробляє винятки exception:
    struct graphicsException : public std::exception{ // новий тип винятку
        const char * what () const { // повідомлення винятку
            return "Graphics exception";
        }
    };

    void exceptionTest(){
        try {
            throw graphicsException();
        }
        catch(graphicsException& e) {
            std::cout << e.what();
        }
        catch(std::exception& e) {
            std::cout << "Not graphicsException";
        }
    }
    Обробка винятків з невідомим типом

    Часто виникає ситуація, що модуль програми чи стороннє API створює винятки, але програмісту є невідомі типи цих винятків. В такому випадку можна використовувати функцію типу exceptionProcessor(), яка виконує узагальнену обробку винятків. Також обробка дозволяє виокремити блоки catch в одну функцію. В наступному коді показано принцип роботи exceptionProcessor(). В exceptionProcessor() використовується команда throw без аргументів. Така команда виконує повторне створення поточного винятку.

    void exceptionProcessor(){ // узагальнений обробник винятків
        try {
            throw; // throw без аргументів, призводить до повторного винекнення винятку,
            // що виник у блоці try функції exceptionTest()
        }
        catch (std::exception & e) {
            // обробка стандартних винятків
        }
        catch (const char * & e) {
            // обробка символьних винятків
        }
        catch (int & e) {
            // обробка числових винятків
        }
        // тут можуть бути блоки catch, для обробки інших типів винятків
        catch(...){
            // не вдалося ідентифікувати виняток, обробка по замовчуванню
        }
    }

    void exceptionTest2(){
        try {
            // код в якому виникають винятки
        }
        catch (...) { // усі винятки будуть оброблятися цим блоком catch
            exceptionProcessor(); // і для кожного винятку буде викликана ця функція
        }
    }

    Блок try може містити блоки try, а ті в свою чергу інші блоки try. Буває необхідно з внутрішнього блоку try повернутися до зовнішнього. Для цього знову ж можна викоритати команду throw без аргументів. Throw у блоці catch призводить до передачі обробки поточного винятку до зовнішнього блоку catch. Таким чином можна створити багато рівнів обробки. Це може знадобитися, коли якась дія глибоко в коді призвела до винятку, і необхідно виконати певну кількість дій на кожному рівні програми для відновлення роботоспособності програми. Наприклад:

    void exceptionThrower(){
        try{ // внутрішній контроль за винятками
            throw 1;
        }
        catch(...){ // внутрішній обробник винятків
            std::cout << "Inner catch";
            throw; // перевід контролю до зовнішнього блоку try
        }
    }

    void exeptionTest3(){
        try{ // зовнішній контроль за винятками
            exceptionThrower();
        }
        catch(...){ // зовнішній обробник винятків
            std::cout << "Outer catch";
        }
    }
    Виняток у конструкторі

    Винятки в конструкторі об'єкта є дозволеними. Якщо виник виняток у конструкторі об'єкта, то сам об'єкт не буде створеним, і немає необхідності викликати для нього деструктор. Усі члени об'єкту, що створені автоматично, будуть автоматично видаленими. Усі об'єкти, що створені у динамічній памяті не будуть видаленими. Якщо ці об'єкти видаляються у деструкторі, то вони не будуть видалені, так як сам об'єкт не є створеним, і його деструктор не буде викликатися. Такі об'єкти потребують використання розумних вказівників.

    Виняток у деструкторі

    Винятки в деструкторі об'єкта не є безпечними. Вони можуть призвести до наступної ситуації. Припустимо, що об'єкт (у якого деструктор генерує виняток) створюється на стеку. Після цього, під час виконання інших дій по якійсь причині виняток. При винекненні винятку, усі об'єкти, що створилися на стеку автоматично видаляються. Для об'єкту викликається деструктор, а в деструкторі виникає новий виняток. Таким чином одночасно для обробки є два винятки. В такому випадку C++ не знає, що необхідно зробити. Можна обробити будь-який з двох винятків, але інший залишиться необробленим. Це як мінімум може призвести до не видаленої пам'яті. В таких випадках C++ викликає функцію terminate() і робота програми завершується. Далі наведений приклад:

    struct Cat{
    ~Cat(){ throw "Ups..."; } // виняток у деструкторі
    };

    void exceptionThrower(){
        try{
            Cat a; // обєкт на стеку з винятком у деструкторі
            throw 1; // виняток, що призводе до видалення обєктів на стеку
        }
        catch(...){
            std::cout << "Hm"; // жоден виняток не обробиться
        }
    }

    Тож якщо виникає виняткова ситуація в деструкторі, то правильною дією буде не створювати виняток, а обробити помилку іншим чином, наприклад, записавши повідомлення в лог-файл.

    Поліморфічні винятки

    Замість простого повідомлення винятком може бути будь-який об'єкт. З простими об'єктами все зрозуміло - такий виняток буде оброблятися в блоці catch, що відповідає типу обєкта. Але що буде відбуватися з поліморфними об'єктами?

    struct Animal{}; // базовий клас
    struct Cat:public Animal{}; // дочірній клас

    void exceptionsTest4(){
        Cat a;
        Animal &b = a;
        try{
            throw b; // створюємо виняток
        }
        catch(Cat & e){}
        catch(Animal & e){} // буде викликано цей обробник
    }

    Буде викликаний обробник для типу об'єкту Animal. Для того, щоб був викликаний обробник типу Cat, необхідно створити поліморфний виняток. Це можна зробити наступним чином:

    struct Animal{ // базовий клас
    public:
        virtual void raise(){ throw *this; } // поліморфний виняток
    };

    struct Cat:public Animal{ // дочірній клас
    public:
        virtual void raise(){ throw *this; } // поліморфний виняток
    };

    void exceptionsTest(){
        Cat a;
        Animal &b = a;

        try{
            b.raise();
        }
        catch(Cat & e){} // буде викликано цей обробник
        catch(Animal & e){}
    }
    Корисні винятки
    Передавайте винятки по ссилці. Якщо виняток переданий по значенню, то відбувається копіювання, а якщо по вказівнику, то невідомо хто має видаляти переданий вказівник.
    Перевірка на те, чи виділення памяті пройшло успішно (bad_alloc)
    try(){
        int *pb = new int[999999];
    }
    catch(std::bad_alloc & e){ }
    Можна обійтися і без винятків:
    int *pb = new (std::nothrow) int[999999];
    if(pb==0) { not allocated }
    Переваги винятків
  • Легший для зрозуміння код, менші затрати та коротший час розробки програми.
  • Розділення основного коду та коду обробки помилок.
  • Важко ігнорувати винятки - необроблений виняток завершує виконання програми.
  • Автоматична обробка винятків і передача в обробник, компілятор сам створює необхідний код.
  • Будь-який об'єкт може бути переданий як виняток.
  • Винятки обробляються по типу. Можливість створювати свої ієрархії винятків.
  • Стекові змінні коректно видаляються (і деструктори викликаються).
  • Коди помилок неможливі в конструкторах.
  • Винятки легко передають додаткову інформацію: __LINE__, __FILE__, опис помилки.
  • Недоліки винятків
  • Програма стає більшою і повільнішою.
  • Швидкість роботи винятків залежитть від компілятора.
  • Можливі втрати пам'яті, якщо не використовуються розумні вказівники.
  • Винятки важчі для початківців. Також не завжди просто проаналізувати роботу винятків.
  • Винятки є мало придатні для систем реального часу (через надмірну кількість перевірок).
  • Важко інтегрувати разом винятки в код, який не використовує винятки.



  • Sun and Black Cat- Ігор Дихта (igor dykhta email) © 2007-2014