Правила идентификации типов исключений. Пользовательские классы исключений

Практический курс по ООП C++: https://stepik.org/a/205781

Смотреть материал на YouTube | RuTube

Продолжаем тему обработки исключений и вначале подробнее рассмотрим, как происходит идентификация типов исключений в блоках catch. Пусть для примера у нас имеется следующий класс для работы с банковскими картами:

class BankCard {
    const int length_number {19};
    std::string number; // 1234-5678-1234-5678
 
    void verify_correct(const std::string& card) const
    {
        if(card.length() != length_number)
            throw length_number;
 
        const char* p = card.c_str();
        for(int i = 0;i < length_number; ++i) {
            if(i == 4 || i == 9 || i == 14) {
                if(p[i] != '-')
                    throw "incorrect format";
            }
            else if(p[i] < '0' || p[i] > '9')
                throw "only numbers are allowed";
        }
    }
 
public:
    void set_number(const std::string& card)
    {
        verify_correct(card);
        number = card;
    }
 
    const std::string& get_number() const { return number; }
};

Здесь приватное поле number является строкой и хранит текущий номер карты. Приватный метод verify_correct выполняет проверку корректности формата номера и если что-то записано не верно, то генерирует различные исключения. Два публичных метода set_number и get_number служат для сохранения и чтения номера банковской карты.

Воспользуемся этим классом в функции main следующим образом:

int main()
{
    BankCard card;
 
    try {
        card.set_number("123-4567-1234-5678");
    }
    catch(int e) {
        std::cout << e << std::endl;
    }
    catch(const char* e) {
        std::cout << e << std::endl;
    }
 
    return 0;
}

В блоке try выполняется критический код (который может сгенерировать исключение) при записи номера карты. И два блока catch для отслеживания исключений типа int и const char*.

Если сейчас запустить программу, то сработает первый блок catch и в консоли отобразится целое число 19. Однако тот же самый тип исключения можно отловить, если прописать следующие варианты:

catch(const int e) ...
catch(const int& e) ...
catch(int& e) ...

Правило здесь такое. Любой тип T или const T можно идентифицировать по самому типу T, или константному типу const T или по ссылке на один из этих типов: T& или const T&. Причем, сам тип T здесь имеет первостепенное значение, если, например, вместо типа int сгенерировать такое же целое число типа long:

throw (long)length_number;

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

Однако если пробрасывается не сам объект, а указатель на него, то константный указатель можно идентифицировать только константным указателем, обычный не сработает. Например, если вместо const char* записать просто char*:

catch(char* e)

то этот блок пропустит исключение типа const char*. Однако, если бросить исключение типа char*:

throw (char *)"only numbers are allowed";

то оно будет поймано этим блоком или, если вернуть const:

catch(const char* e)

то и таким.

В результате, мы получаем следующие правила идентификации типов исключений:

Тип в throw

Идентифицируемый тип в catch

T; const T

T; const T; T&; const T&

T*

T*; const T*

const T*

const T*

Использование собственных классов (типов) исключений

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

Вначале объявим следующие классы для описания пользовательских типов исключений при работе с банковскими картами:

class CardError {
    std::string msg;
public:
    CardError(const char* error) : msg(error)
        { }
    CardError(const CardError& other) : msg(other.msg)
        { }
    const char * what() const noexcept { return msg.c_str(); }
};
 
class CardLengthError : public CardError {
public:
    CardLengthError(const char* error) : CardError(error)
        { }
};
 
class CardFormatError : public CardError {
public:
    CardFormatError(const char* error) : CardError(error)
        { }
};
 
class CardNumberError : public CardError {
public:
    CardNumberError(const char* error) : CardError(error)
        { }
};

Ключевое слово noexcept используется, чтобы подчеркнуть факт отсутствия генерации каких-либо исключений методом what.

Пропишем классы в методе verify_correct для генерации соответствующих исключений:

class BankCard {
    const int length_number {19};
    std::string number; // 1234-5678-1234-5678
 
    void verify_correct(const std::string& card) const
    {
        if(card.length() != length_number)
            throw CardLengthError("incorrect length card number");
 
        const char* p = card.c_str();
        for(int i = 0;i < length_number; ++i) {
            if(i == 4 || i == 9 || i == 14) {
                if(p[i] != '-')
                    throw CardFormatError("incorrect format");
            }
            else if(p[i] < '0' || p[i] > '9')
                throw CardNumberError("only numbers are allowed");
        }
    }
...
};

Теперь, как вы уже догадались, в функции main для определения того или иного типа исключения нам нужно использовать эти новые классы, например, следующим образом:

int main()
{
    BankCard card;
 
    try {
        card.set_number("123a-4567-1234-5678");
    }
    catch(CardLengthError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(CardFormatError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(CardNumberError& e) {
        std::cout << e.what() << std::endl;
    }
 
    return 0;
}

Видите, насколько более понятным становится текст программы? Мало того, вместо одного типа исключения const char* у нас появляется два независимых: CardFormatError и CardNumberError. И количество этих типов может быть любым, столько, сколько нужно для корректного описания всевозможных ошибочных ситуаций.

А теперь одна из ключевых особенностей использования классов в блоках catch. Благодаря наследованию всех классов от базового класса CardError, мы можем все эти типы идентифицировать им одним, если прописать:

int main()
{
    BankCard card;
 
    try {
        card.set_number("123a-4567-1234-5678");
    }
    catch(CardError& e) {
        std::cout << "CardError: " << e.what() << std::endl;
    }
    catch(CardLengthError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(CardFormatError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(CardNumberError& e) {
        std::cout << e.what() << std::endl;
    }
 
    return 0;
}

Теперь любое исключение типов CardLengthError, CardFormatError и CardNumberError будет отлавливаться первым блоком catch, а остальные игнорироваться. Конечно, это не то, что нам нужно, поэтому тип с базовыми классами прописывают в самом конце:

 int main()
{
    BankCard card;
 
    try {
        card.set_number("123a-4567-1234-5678");
    }
    catch(CardLengthError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(CardFormatError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(CardNumberError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(CardError& e) {
        std::cout << "CardError: " << e.what() << std::endl;
    }
 
    return 0;
}

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

int main()
{
    BankCard card;
 
    try {
        card.set_number("123a-4567-1234-5678");
    }
    catch(const CardLengthError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(const CardFormatError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(const CardNumberError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(const CardError& e) {
        std::cout << "CardError: " << e.what() << std::endl;
    }
 
    return 0;
}

Такое определение типов исключений с использованием классов будет наиболее корректным.

Базовый класс исключений std::exception

И самый последний штрих, который здесь следует сделать – это унаследовать класс CardError от стандартного класса std::exception, который в языке C++ принято использовать, как базовый для всех остальных классов исключений:

class CardError : public std::exception {
    std::string msg;
public:
    CardError(const char* error) : msg(error)
        { }
    const char * what() const noexcept override { return msg.data(); }
};

Если посмотреть на содержимое класса std::exception, то в упрощенном виде его можно представить следующим образом:

namespace std
{
    class exception
    {
    public:
        exception() noexcept;
        exception(const exception&) noexcept;
        exception& operator=(const exception&) noexcept;
        virtual ~exception(); // Destructor
        virtual const char* what() const noexcept; // возвращает сообщение об исключении
    };
}

Видим, что здесь имеются все необходимые конструкторы и деструкторы, операция присваивания копированием и специальный виртуальный метод what, который принято использовать для отображения возникшей ошибки. Именно поэтому в нашем классе CardError имеется переопределение этого метода. Теперь, благодаря такой цепочке наследования, для отлавливания произвольных исключений в самом конце можно записать блок catch с типом std::exception:

int main()
{
    BankCard card;
 
    try {
        card.set_number("123a-4567-1234-5678");
    }
    catch(const CardLengthError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(const CardFormatError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(const CardNumberError& e) {
        std::cout << e.what() << std::endl;
    }
    catch(const CardError& e) {
        std::cout << "CardError: " << e.what() << std::endl;
    }
    catch(const std::exception& e) {
        std::cout << e.what() << std::endl;
    }
 
    return 0;
}

Заключение

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

Тип в throw

Идентифицируемый тип в catch

T; const T

T; const T; T&; const T&

T*

T*; const T*

const T*

const T*

При наследовании B : A (A – базовый класс или структура; B – дочерний)

A; const A

A; const A; A&; const A&

B; const B

B; const B; B&; const B&;

A& const A&

A*

A*; const A*

const A*

const A*

B*

B*; const B*; A*; const A*

const B*

const B*; const A*

Практический курс по ООП C++: https://stepik.org/a/205781

Видео по теме