Меню

Каким образом реализуют наследование классов ошибок

Добавлено 11 сентября 2021 в 12:17

Исключения и функции-члены

До этого момента в данном руководстве вы видели исключения, используемые только в функциях, не являющихся членами. Однако исключения также полезны в функциях-членах и тем более в перегруженных операторах. Рассмотрим следующий перегруженный оператор [] как часть простого класса целочисленного массива:

int& IntArray::operator[](const int index)
{
    return m_data[index];
}

Хотя эта функция будет отлично работать, пока index является допустимым индексом массива, ей очень не хватает проверки на ошибку. Мы могли бы добавить инструкцию assert, чтобы убедиться, что index корректен:

int& IntArray::operator[](const int index)
{
    assert (index >= 0 && index < getLength());
    return m_data[index];
}

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

int& IntArray::operator[](const int index)
{
    if (index < 0 || index >= getLength())
        throw index;

    return m_data[index];
}

Теперь, если пользователь передает недопустимый индекс, operator[] вызовет исключение типа int.

Когда конструкторы дают сбой

Конструкторы – это еще одна область классов, в которой исключения могут быть очень полезны. Если по какой-либо причине конструктор может дать сбой (например, пользователь передал недопустимые входные данные), просто выбросите исключение, чтобы указать, что объект не удалось создать. В таком случае создание объекта прерывается, и все члены класса (которые уже были созданы и инициализированы до выполнения тела конструктора) уничтожаются как обычно.

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

Это приводит к вопросу о том, что мы должны делать, если мы выделили ресурсы в нашем конструкторе, а затем возникает исключение до завершения конструктора. Как обеспечить правильное освобождение уже выделенных ресурсов? Один из способов – обернуть любой код, который может дать сбой в блок try, использовать соответствующий блок catch для перехвата исключения и выполнить любую необходимую очистку, а затем повторно выбросить исключение (эту тему мы обсудим в уроке «20.6 – Повторное выбрасывание исключений»). Однако это добавляет много бардака, и здесь легко ошибиться, особенно если ваш класс выделяет несколько ресурсов.

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

Например:

#include <iostream>

class Member
{
public:
    Member()
    {
        std::cerr << "Member allocated some resourcesn";
    }

    ~Member()
    {
        std::cerr << "Member cleaned upn";
    }
};

class A
{
private:
    int m_x {};
    Member m_member;

public:
    A(int x) : m_x{x}
    {
        if (x <= 0)
            throw 1;
    }

    ~A()
    {
        std::cerr << "~An"; // не должен вызываться
    }
};


int main()
{
    try
    {
        A a{0};
    }
    catch (int)
    {
        std::cerr << "Oopsn";
    }

    return 0;
}

Этот код печатает:

Member allocated some resources
Member cleaned up
Oops

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

Это одна из причин того, что RAII (метод, описанный в уроке «12.9 – Деструкторы») так широко пропагандируется – даже в исключительных обстоятельствах классы, реализующие RAII, могут выполнять после себя очистку.

Однако создание пользовательского класса, такого как Member, для управления размещением ресурсов неэффективно. К счастью, стандартная библиотека C++ поставляется с RAII-совместимыми классами для управления распространенными типами ресурсов, такими как файлы (std::fstream, рассмотренные в уроке «23.6 – Основы файлового ввода/вывода») и динамическая память (std::unique_ptr и другие умные указатели, описанные в «M.1 – Введение в умные указатели и семантику перемещения»).

Например, вместо этого:

class Foo
private:
    int *ptr; // Foo будет обрабатывать выделение/освобождение

Сделайте так:

class Foo
private:
    std::unique_ptr<int> ptr; // std::unique_ptr будет обрабатывать выделение/освобождение

В первом случае, если конструктор Foo завершится со сбоем после того, как ptr выделил свою динамическую память, Foo будет отвечать за очистку, что может быть сложной задачей. Во втором случае, если конструктор Foo завершится со сбоем после того, как ptr выделил свою динамическую память, деструктор ptr выполнит и вернет эту память в систему. Foo не должен выполнять какую-либо явную очистку, когда обработка ресурсов делегируется членам, совместимым с RAII!

Классы исключений

Одна из основных проблем с использованием базовых типов данных (таких как int) в качестве типов исключений заключается в том, что они по своей сути непонятны. Еще бо́льшая проблема – это разрешение неоднозначности того, что означает исключение, когда в блоке try есть несколько инструкций или вызовов функций.

// Использование перегруженного operator[] класса IntArray
// из примера выше

try
{
    int* value{ new int{ array[index1] + array[index2]} };
}
catch (int value)
{
    // Что мы здесь ловим?
}

В этом примере, если бы мы перехватили исключение типа int, о чем это нам сказало бы? Был ли один из индексов массива вне допустимого диапазона? operator+ вызвал целочисленное переполнение? Сбой оператора new из-за нехватки памяти? К сожалению, в этом случае нет простого способа устранить неоднозначность. Хотя мы можем генерировать исключения const char* для решения проблемы определения, ЧТО пошло не так, это всё же не дает нам возможности обрабатывать исключения из разных источников по-разному.

Один из способов решить эту проблему – использовать классы исключений. Класс исключения – это просто обычный класс, специально созданный для выдачи исключения. Давайте спроектируем простой класс исключения, который будет использоваться с нашим классом IntArray:


#include <string>
#include <string_view>

class ArrayException
{
private:
    std::string m_error;

public:
    ArrayException(std::string error)
        : m_error{ error }
    {
    }

    std::string_view getError() const { return m_error; }
// В C++14 или более ранней версии, используйте вместо этого следующее
//    const char* getError() const { return m_error.c_str(); } 
};

Вот полный код программы, использующей этот класс:

#include <iostream>
#include <string>
#include <string_view>

class ArrayException
{
private:
    std::string m_error;

public:
    ArrayException(std::string error)
        : m_error{ error }
    {
    }

    std::string_view getError() const { return m_error; }
// В C++14 или более ранней версии, используйте вместо этого следующее
//    const char* getError() const { return m_error.c_str(); } 
};

class IntArray
{
private:

    int m_data[3]{}; // для простоты предполагаем, что массив имеет длину 3
public:
    IntArray() {}

    int getLength() const { return 3; }

    int& operator[](const int index)
    {
        if (index < 0 || index >= getLength())
            throw ArrayException{ "Invalid index" };

        return m_data[index];
    }

};

int main()
{
    IntArray array;

    try
    {
        int value{ array[5] }; // индекс вне допустимого диапазона
    }
    catch (const ArrayException& exception)
    {
        std::cerr << "An array exception occurred (" << exception.getError() << ")n";
    }
}

Используя такой класс, мы можем сделать так, чтобы исключение вернуло описание возникшей проблемы, которое предоставляет контекст для того, что пошло не так. А поскольку ArrayException – это собственный уникальный тип, мы можем специально перехватывать исключения, создаваемые классом массива, и при желании обрабатывать их иначе, чем другие исключения.

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

Исключения и наследование

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

class Base
{
public:
    Base() {}
};

class Derived: public Base
{
public:
    Derived() {}
};

int main()
{
    try
    {
        throw Derived();
    }
    catch (const Base& base)
    {
        std::cerr << "caught Base";
    }
    catch (const Derived& derived)
    {
        std::cerr << "caught Derived";
    }

    return 0;
}

В приведенном выше примере мы генерируем исключение типа Derived. Однако результат этой программы:

caught Base

Что случилось?

Во-первых, как упоминалось выше, производные классы будут перехвачены обработчиками базового типа. Поскольку Derived является производным от Base, Derived «является» Base (между ними есть связь «является чем-либо»). Во-вторых, когда C++ пытается найти обработчик возникшего исключения, он делает это последовательно. Следовательно, первое, что делает C++, – это проверяет, соответствует ли обработчик исключений для Base исключению Derived. Поскольку Derived «является» Base, ответ – да, и он выполняет блок catch для типа Base! Блок catch для Derived в этом случае даже не проверяется.

Чтобы этот пример работал, как задумывалось, нам нужно изменить порядок блоков catch:

class Base
{
public:
    Base() {}
};

class Derived: public Base
{
public:
    Derived() {}
};

int main()
{
    try
    {
        throw Derived();
    }
    catch (const Derived& derived)
    {
        std::cerr << "caught Derived";
    }
    catch (const Base& base)
    {
        std::cerr << "caught Base";
    }

    return 0;
}

Таким образом, обработчик Derived получит первым шанс перехватить объекты типа Derived (до того, как это сделает обработчик для Base). Объекты типа Base не будут соответствовать обработчику Derived (Derived «является» Base, но Base не является Derived) и, таким образом, «провалятся вниз» до обработчика Base.

Правило


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

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

std::exception

Многие классы и операторы в стандартной библиотеке в случае сбоя выдают исключения с объектами классов. Например, оператор new может выбросить std::bad_alloc, если не может выделить достаточно памяти. Неудачный dynamic_cast вызовет std::bad_cast. И так далее. Начиная с C++20, существует 28 различных классов исключений, которые могут быть сгенерированы, и в каждом последующем стандарте языка добавляется еще больше.

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

В большинстве случаев, когда стандартная библиотека генерирует исключение, нас не волнует, неудачное ли это выделение памяти, неправильное приведение или что-то еще. Нас просто волнует, что что-то катастрофически пошло не так, и теперь наша программа дает сбой. Благодаря std::exception мы можем настроить обработчик исключений для перехвата исключений типа std::exception, и в итоге мы перехватим и std::exception, и все производные исключения в одном месте. Всё просто!

#include <cstddef>   // для std::size_t
#include <iostream>
#include <exception> // для std::exception
#include <limits>
#include <string>    // для this example

int main()
{
    try
    {
        // Здесь идет ваш код, использующий стандартную библиотеку.
        // Для примера мы намеренно вызываем одно из ее исключений.
        std::string s;
        // вызовет исключение std::length_error или исключение выделения памяти
        s.resize(std::numeric_limits<std::size_t>::max());
    }
    // Этот обработчик перехватит std::exception и все производные от него исключения
    catch (const std::exception& exception)
    {
        std::cerr << "Standard exception: " << exception.what() << 'n';
    }

    return 0;
}

Приведенная выше программа печатает:

Standard exception: string too long

Приведенный выше пример довольно простой. В нем стоит отметить одну вещь: std::exception имеет виртуальную функцию-член what(), которая возвращает строку в стиле C с описанием исключения. Большинство производных классов переопределяют функцию what() для изменения этого сообщения. Обратите внимание, что эта строка предназначена для использования только для описательного текста – не используйте ее для сравнений, поскольку не гарантируется, что она будет одинаковой для разных компиляторов.

Иногда нам нужно обрабатывать определенный тип исключения по-другому. В этом случае мы можем добавить обработчик для этого конкретного типа и позволить всем остальным «проваливаться вниз» до обработчика базового типа. Рассмотрим следующий код:

try
{
     // здесь идет код, использующий стандартную библиотеку
}
// Этот обработчик здесь перехватит std::length_error
// (и любые производные от него исключения)
catch (const std::length_error& exception)
{
    std::cerr << "You ran out of memory!" << 'n';
}
// Этот обработчик перехватит std::exception (и любое исключение,
// производное от него), которое "провалится" сюда
catch (const std::exception& exception)
{
    std::cerr << "Standard exception: " << exception.what() << 'n';
}

В этом примере исключения типа std::length_error будут перехвачены и обработаны первым обработчиком. Исключения типа std::exception и всех других производных классов будут перехвачены вторым обработчиком.

Такие иерархии наследования позволяют нам использовать определенные обработчики для нацеливания на определенные производные классы исключений или использовать обработчики базовых классов для перехвата всей иерархии исключений. Это позволяет нам точно контролировать, какие исключения мы хотим обрабатывать, и при этом нам не нужно делать слишком много работы, чтобы отловить «всё остальное» в иерархии.

Использование стандартных исключений напрямую

Ничто напрямую не вызывает std::exception, и вы тоже. Однако вы можете свободно использовать другие стандартные классы исключений из стандартной библиотеки, если они адекватно соответствуют вашим потребностям. Список всех стандартных исключений вы можете найти на cppreference.

std::runtime_error (включен как часть заголовка stdexcept) выбирается часто потому, что он имеет обобщенное название, а его конструктор принимает настраиваемое сообщение:

#include <iostream>
#include <stdexcept> // для std::runtime_error

int main()
{
    try
    {
        throw std::runtime_error("Bad things happened");
    }
    // Этот обработчик перехватит std::exception
    // и все производные от него исключения
    catch (const std::exception& exception)
    {
        std::cerr << "Standard exception: " << exception.what() << 'n';
    }

    return 0;
}

Этот код печатает:

Standard exception: Bad things happened

Создание собственных классов, производных от std::exception или std::runtime_error

Конечно, вы можете наследовать свои классы от std::exception и переопределять виртуальную константную функцию-член what(). Вот та же программа, что и выше, но с исключением ArrayException, производным от std::exception:

#include <exception> // для std::exception
#include <iostream>
#include <string>
#include <string_view>

class ArrayException : public std::exception
{
private:
    std::string m_error{}; // обрабатываем нашу строку

public:
    ArrayException(std::string_view error)
        : m_error{error}
    {
    }

    // std::exception::what() возвращает const char*,
    // поэтому мы должны делать так же, как она
    const char* what() const noexcept override { return m_error.c_str(); }
};

class IntArray
{
private:

    int m_data[3] {}; // для простоты предполагаем, что массив имеет длину 3
public:
    IntArray() {}

    int getLength() const { return 3; }

    int& operator[](const int index)
    {
        if (index < 0 || index >= getLength())
            throw ArrayException("Invalid index");

        return m_data[index];
    }

};

int main()
{
    IntArray array;

    try
    {
        int value{ array[5] };
    }
    catch (const ArrayException& exception) // блоки catch с производными классами идут первыми
    {
        std::cerr << "An array exception occurred (" << exception.what() << ")n";
    }
    catch (const std::exception& exception)
    {
        std::cerr << "Some other std::exception occurred (" << exception.what() << ")n";
    }
}

Обратите внимание, что виртуальная функция what() имеет спецификатор noexcept (что означает, что эта функция обещает не генерировать исключения). Следовательно, у нашего переопределения также должен быть спецификатор noexcept.

Поскольку std::runtime_error уже имеет возможности обработки строк, он также популярен в качестве базового класса для производных классов исключений. Вот тот же пример, использующий std::runtime_error:

#include <stdexcept> // для std::runtime_error
#include <iostream>
#include <string>

class ArrayException : public std::runtime_error
{
private:

public:
    // std::runtime_error принимает строку const char* с завершающим нулем.
    // std::string_view не может оканчиваться нулем, поэтому здесь это не лучший выбор.
    // Наше исключение ArrayException примет вместо него const std::string&,
    // которая гарантированно оканчивается нулем и может быть преобразована
    // в const char*.
    ArrayException(const std::string &error)
        : std::runtime_error{ error.c_str() } // std::runtime_error обработает эту строку
    {
    }

        // не нужно переопределять what(),
        // так как мы можем просто использовать std::runtime_error::what()
};

class IntArray
{
private:

    int m_data[3]{}; // для простоты предполагаем, что массив имеет длину 3
public:
    IntArray() {}

    int getLength() const { return 3; }

    int& operator[](const int index)
    {
        if (index < 0 || index >= getLength())
            throw ArrayException("Invalid index");

        return m_data[index];
    }

};

int main()
{
    IntArray array;

    try
    {
        int value{ array[5] };
    }
    catch (const ArrayException& exception) // блоки catch с производными классами идут первыми
    {
        std::cerr << "An array exception occurred (" << exception.what() << ")n";
    }
    catch (const std::exception& exception)
    {
        std::cerr << "Some other std::exception occurred (" << exception.what() << ")n";
    }
}

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

Теги

C++ / CppException / ИсключениеLearnCppRAII / Resource Acquisition Is Initialization / Получение ресурса есть инициализацияstd::exceptionstd::runtime_errorSTL / Standard Template Library / Стандартная библиотека шаблоновДля начинающихКласс (программирование)НаследованиеОбработка ошибокОбучениеПрограммирование

ВикиЧтение

C++ для начинающих
Липпман Стенли

19.2. Исключения и наследование

Обработка исключений – это стандартное языковое средство для реакции на аномальное поведение программы во время выполнения. C++ поддерживает единообразный синтаксис и стиль обработки исключений, а также способы тонкой настройки этого механизма в специальных ситуациях. Основы его поддержки в языке C++ описаны в главе 11, где показано, как программа может возбудить исключение, передать управление его обработчику (если таковой существует) и как обработчики исключений ассоциируются с try-блоками.

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

19.2.1. Исключения, определенные как иерархии классов

В главе 11 мы использовали два типа класса для описания исключений, возбуждаемых функциями-членами нашего класса iStack:

class popOnEmpty { … };

class pushOnFull { … };

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

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

class Excp { … };

class popOnEmpty : public Excp { … };

class pushOnFull : public Excp { … };

Одной из операцией, которые предоставляет базовый класс, является вывод сообщения об ошибке. Эта возможность используется обоими классами, стоящими ниже в иерархии:

class Excp {

public:

// напечатать сообщение об ошибке

static void print( string msg ) {

cerr

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

class Excp { … };

class stackExcp : public Excp { … };

class popOnEmpty : public stackExcp { … };

class pushOnFull : public stackExcp { … };

class mathExcp : public Excp ( … };

class zeroOp : public mathExcp { … };

class divideByZero : public mathExcp { … };

Последующие уточнения позволяют более детально идентифицировать аномальные ситуации в работе программы. Дополнительные классы исключений организуются как слои. По мере углубления иерархии каждый новый слой описывает все более специфичные исключения. Например, первый, самый общий слой в приведенной выше иерархии представлен классом Excp. Второй специализирует Excp, выделяя из него два подкласса: stackExcp (для исключений при работе с нашим iStack) и mathExcp (для исключений, возбуждаемых функциями из математической библиотеки). Третий, самый специализированный слой данной иерархии уточняет классы исключений: popOnEmpty и pushOnFull определяют два вида исключений работы со стеком, а ZeroOp и divideByZero – два вида исключений математических операций.

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

Читайте также

Исключения

Исключения
СОМ имеет специфическую поддержку выбрасывания (throwing) исключительных ситуаций из реализации методов. Поскольку в языке C++ не существует двоичного стандарта для исключений, СОМ предлагает явные API-функции для выбрасывания и перехвата объектов СОМ-исключений://

2. Наследование

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

Правило 34: Различайте наследование интерфейса и наследование реализации

Правило 34: Различайте наследование интерфейса и наследование реализации
Внешне простая идея открытого наследования при ближайшем рассмотрении оказывается состоящей из двух различных частей: наследования интерфейса функций и наследования их реализации. Различие

1.1.2. Наследование

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

Наследование

Наследование
Следующим принципом ООП является наследование, означающее способность языка обеспечить построение определений новых классов на основе определений существующих классов. В сущности, наследование позволяет расширить возможности поведения базового класса

Наследование форм

Наследование форм
Одним из наиболее привлекательных аспектов построения диалоговых окон в Windows Forms является наследование форм. Вы, несомненно, знаете, что наследование является одним из базовых принципов ООП, который позволяет одному классу расширить функциональность

Наследование конфигурации

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

Исключения

Исключения
Обработчики исключений могут быть написаны, чтобы «съесть» ошибку, обрабатывая ее разными способами. Например, в итеративной подпрограмме входная строка, вызывающая исключение, необязательно должна приводить к остановке всего процесса. Обработка исключения

Наследование

Наследование
Пожалуй, самая важная возможность, предоставляемая программисту средствами языка Си++, заключается в механизме наследования. Вы можете наследовать от определенных ранее классов новые производные классы. Класс, от которого происходит наследование,

Тип исключения

Тип исключения
Если вызывается исключение, для которого отсутствует обработчик и не определен универсальный обработчик исключений всех типов, тогда вызывается функция terminate из стандартной библиотеки. Она вызывает функцию abort, завершающую работу программы.Вы можете

Наследование

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

Исключения

Исключения
Вооружившись понятием отказа, можно теперь определить понятие «исключение». Программа приводит к отказу из-за возникновения некоторых специфических событий (арифметического переполнения, нарушения спецификаций), прерывающих ее выполнение. Такие события и

Наследование и утверждения

Наследование и утверждения
Следствия красоты базисных идей:[x]. Связь наследования с утверждениями и Проектированием по Контракту.[x]. Глобальная структура наследования, где все классы согласованы.[x]. Замороженные компоненты, для которых не применим принцип

26. Наследование

26. Наследование
Наследование – это процесс порождения новых типов-потомков от существующих типов-родителей, при этом потомок получает (наследует) от родителя все его поля и методы.Тип-потомок, при этом, называется наследником или порожденным (дочерним) типом. А тип,

Though this question is rather old and has already been answered plenty, I just want to add a note on how to do proper exception handling in C++11, since I am continually missing this in discussions about exceptions:

Use std::nested_exception and std::throw_with_nested

It is described on StackOverflow here and here, how you can get a backtrace on your exceptions inside your code without need for a debugger or cumbersome logging, by simply writing a proper exception handler which will rethrow nested exceptions.

Since you can do this with any derived exception class, you can add a lot of information to such a backtrace!
You may also take a look at my MWE on GitHub, where a backtrace would look something like this:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

You don’t even need to subclass std::runtime_error in order to get plenty of information when an exception is thrown.

The only benefit I see in subclassing (instead of just using std::runtime_error) is that your exception handler can catch your custom exception and do something special. For example:

try
{
  // something that may throw
}
catch( const MyException & ex )
{
  // do something specialized with the
  // additional info inside MyException
}
catch( const std::exception & ex )
{
  std::cerr << ex.what() << std::endl;
}
catch( ... )
{
  std::cerr << "unknown exception!" << std::endl;
}

Though this question is rather old and has already been answered plenty, I just want to add a note on how to do proper exception handling in C++11, since I am continually missing this in discussions about exceptions:

Use std::nested_exception and std::throw_with_nested

It is described on StackOverflow here and here, how you can get a backtrace on your exceptions inside your code without need for a debugger or cumbersome logging, by simply writing a proper exception handler which will rethrow nested exceptions.

Since you can do this with any derived exception class, you can add a lot of information to such a backtrace!
You may also take a look at my MWE on GitHub, where a backtrace would look something like this:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

You don’t even need to subclass std::runtime_error in order to get plenty of information when an exception is thrown.

The only benefit I see in subclassing (instead of just using std::runtime_error) is that your exception handler can catch your custom exception and do something special. For example:

try
{
  // something that may throw
}
catch( const MyException & ex )
{
  // do something specialized with the
  // additional info inside MyException
}
catch( const std::exception & ex )
{
  std::cerr << ex.what() << std::endl;
}
catch( ... )
{
  std::cerr << "unknown exception!" << std::endl;
}

В
С#
имеется возможность обрабатывать
исключения, создаваемые программистом.
В своих программах вы можете использовать
для обработки ошибок «собственные»
исключения. Для этого достаточно
определить класс как производный от
класса Exception.
Как правило, определяемые программистом
исключения, должны быть производными
от класса ApplicationException,
«родоначальника» иерархии,
зарезервированной для исключений,
связанных с прикладными программами.
Созданные вами производные классы не
должны ничего реализовывать, поскольку
одно лишь их существование в системе
типов уже позволит использовать их в
качестве исключений.

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

//
Создание пользовательского исключения
для

//
обнаружения ошибок при работе с объектами
класса

//
RangeArray.

using
System;

//
Создаем исключение для класса RangeArray.

class
RangeArrayException : ApplicationException {

//
Реализуемстандартныеконструкторы,

public
RangeArrayException() : base() { }

public
RangeArrayException(string str) : base(str) { }

//
Переопределяемметод
ToString()
длякласса

//
RangeArrayException.

public
override string ToString() {

return
Message;

}}

//
Улучшенная версия класса RangeArray.

class
RangeArray {

//
Закрытые данные.

int[]
а; // Ссылка на базовый массив.

intlowerBound;
// Наименьший индекс.

intupperBound;
// Наибольший индекс.

intlen;
// Базовая переменная для свойства
Length.

//
Создаем массив с заданным размером,

public
RangeArray(int low, int high) {

high++;

if(high
<= low) {

throw
new RangeArrayException(

«Нижний
индекс не меньше верхнего.»),

}

а
= new int[high — low];

len
= high — low;

lowerBound
= low;

upperBound
= —
high;

}

//
Свойство
Length,
предназначенное только для чтения,

public
int Length {

get
{

return
len;

}}

//
Индексатор для объекта класса
RangeArray.

public
int this[int index] {

//
Средство для чтения элемента массива,

get
{

if(ok(index))
{

return
a[index — lowerBound];

}
else {

throw
new RangeArrayException(

«Ошибка
нарушения границ диапазона.»);

}}

//
Средство для записи элемента массива.-

set
{

if(ok(index))
{

a[index
— lowerBound] = value;

}

else
throw new RangeArrayException(

«Ошибка
нарушения границ диапазона.»);

}}

//
Метод возвращает значение
true,

//
если индекс в пределах диапазона,

private
bool ok(int index) {

if(index
>= lowerBound & index <= upperBound)

returntrue;

returnfalse;

}}

//
Демонстрируем использование массива
с заданным

//
диапазоном изменения индекса,

classRangeArrayDemo
{

public
static
void
Main()
{

try
{

RangeArray
ra = new RangeArray(-5, 5);

RangeArrayra2
=
newRangeArray(1,
10);

//
Демонстрируем использование объекта-массива
га.

Console.WriteLine(«Длина
массива га: » +
ra.Length);

for(int
i = -5; i <= 5; i++)

ra[i]
= i;

Console.Write(«Содержимое
массива
ra:
» ) ;

for(int
i = -5; i <= 5; i++)

Console.Write(ra[i]
+ » » ) ;

Console.WriteLinen«);

//
Демонстрируем использование объекта-массива
ra2.

Console.WriteLine(«Длина
массива ra2: » + ra2.Length);

for(int
i = 1; i <= 10; i++)

ra2[i]
= i;

Console.Write(«Содержимое
массива
ra2:
«) ;

for(int
i = 1; i <= 10; i++)

Console.Write(ra2[i]
+ » » ) ;

Console.WriteLine(«n»);

}
catch (RangeArrayException exc) {

Console.WriteLine(exc);

}

//
Теперь демонстрируем «работу над
ошибками».

Console.WriteLine(

«Сгенерируем
ошибки непопадания в диапазон.»);

//
Используем неверно заданный конструктор,

try
{

RangeArray
ra3= new RangeArray(100, -10); // Ошибка!

}
catch (RangeArrayException exc) {

Console.WriteLine(exc);

}

//
Используем неверно заданный индекс,

try
{

RangeArray
ra3 = new RangeArray(-2, 2);

for(int
i = -2; i <= 2; i++)

гаЗ
[i] = i;

Console.Write(«Содержимое
массива
ra3: » ) ;

for(int
i = -2; i <= 10; i++) // Ошибка непопадания

//
в диапазон.

Console.Write(ra3[i]
+ » » ) ;

}
catch (RangeArrayException exc){

Console.WriteLine(exc);

}}

При
выполнении этой программы получаем
такие результаты:

Длина
массива
ra:
11

Содержимое
массива
ra:
— 5 — 4 — 3 — 2 — 1 0 1 2 3 4 5

Длина
массива
ra2:
10

С
о д е р ж и м о е м а с с и в а
ra2
: 1 2 3 4 5 6 7 8 9 1 0

Сгенерируем
ошибки непопадания в диапазон.

Нижний
индекс не меньше верхнего.

Содержимое
массива
raЗ:
— 2 — 1 0 1 2 Ошибка нарушения границ диапазона.

При
возникновении ошибки нарушения границ
диапазона RangeArray-объект генерирует
объект типа RangeArrayException. Этот класс —
производный от классаApplicationException. Класс
исключений, создаваемыйпрограммистом,
обычно выводится из класса
ApplicationException. Обратите внимание на то,
что подобная ошибка может обнаружиться
во время созданияRangeArray-объекта. Чтобы
перехватывать такие исключения, объекты
классаRange Array должны создаваться внутри
блока try, как это показано в программе.
Сиспользованием исключений для сообщений
об ошибках класс RangeArray теперьнапоминает
один из встроенных С#-типов и может быть
полностью интегрирован вмеханизм
обработки исключений любой программы.

В C++ различают ошибки времени компиляции и ошибки времени выполнения. Ошибки первого типа обнаруживает компилятор до запуска программы. К ним относятся, например, синтаксические ошибки в коде. Ошибки второго типа проявляются при запуске программы. Примеры ошибок времени выполнения: ввод некорректных данных, некорректная работа с памятью, недостаток места на диске и т. д. Часто такие ошибки могут привести к неопределённому поведению программы.

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

Коды возврата и исключения

Рассмотрим функцию, которая считывает со стандартного потока возраст и возвращает его вызывающей стороне. Добавим в функцию проверку корректности возраста: он должен находиться в диапазоне от 0 до 128 лет. Предположим, что повторный ввод возраста в случае ошибки не предусмотрен.

int ReadAge() {
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        // Что вернуть в этом случае?
    }
    return age;
}

Что вернуть в случае некорректного возраста? Можно было бы, например, договориться, что в этом случае функция возвращает ноль. Но тогда похожая проверка должна быть и в месте вызова функции:

int main() {
    if (int age = ReadAge(); age == 0) {
        // Произошла ошибка
    } else {
        // Работаем с возрастом age
    }
}

Такая проверка неудобна. Более того, нет никакой гарантии, что в вызывающей функции программист вообще её напишет. Фактически мы тут выбрали некоторое значение функции (ноль), обозначающее ошибку. Это пример подхода к обработке ошибок через коды возврата. Другим примером такого подхода является хорошо знакомая нам функция main. Только она должна возвращать ноль при успешном завершении и что-либо ненулевое в случае ошибки.

Другим способом сообщить об обнаруженной ошибке являются исключения. С каждым сгенерированным исключением связан некоторый объект, который как-то описывает ошибку. Таким объектом может быть что угодно — даже целое число или строка. Но обычно для описания ошибки заводят специальный класс и генерируют объект этого класса:

#include <iostream>

struct WrongAgeException {
    int age;
};

int ReadAge() {
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        throw WrongAgeException(age);
    }
    return age;
}

Здесь в случае ошибки оператор throw генерирует исключение, которое представлено временным объектом типа WrongAgeException. В этом объекте сохранён для контекста текущий неправильный возраст age. Функция досрочно завершает работу: у неё нет возможности обработать эту ошибку, и она должна сообщить о ней наружу. Поток управления возвращается в то место, откуда функция была вызвана. Там исключение может быть перехвачено и обработано.

Перехват исключения

Мы вызывали нашу функцию ReadAge из функции main. Обработать ошибку в месте вызова можно с помощью блока try/catch:

int main() {
    try {
        age = ReadAge();  // может сгенерировать исключение
        // Работаем с возрастом age
    } catch (const WrongAgeException& ex) {  // ловим объект исключения
        std::cerr << "Age is not correct: " << ex.age << "n";
        return 1;  // выходим из функции main с ненулевым кодом возврата
    }
    // ...
}

Мы знаем заранее, что функция ReadAge может сгенерировать исключение типа WrongAgeException. Поэтому мы оборачиваем вызов этой функции в блок try. Если происходит исключение, для него подбирается подходящий catch-обработчик. Таких обработчиков может быть несколько. Можно смотреть на них как на набор перегруженных функций от одного аргумента — объекта исключения. Выбирается первый подходящий по типу обработчик и выполняется его код. Если же ни один обработчик не подходит по типу, то исключение считается необработанным. В этом случае оно пробрасывается дальше по стеку — туда, откуда была вызвана текущая функция. А если обработчик не найдётся даже в функции main, то программа аварийно завершается.

Усложним немного наш пример, чтобы из функции ReadAge могли вылетать исключения разных типов. Сейчас мы проверяем только значение возраста, считая, что на вход поступило число. Но предположим, что поток ввода досрочно оборвался, или на входе была строка вместо числа. В таком случае конструкция std::cin >> age никак не изменит переменную age, а лишь возведёт специальный флаг ошибки в объекте std::cin. Наша переменная age останется непроинициализированной: в ней будет лежать неопределённый мусор. Можно было бы явно проверить этот флаг в объекте std::cin, но мы вместо этого включим режим генерации исключений при таких ошибках ввода:

int ReadAge() {
    std::cin.exceptions(std::istream::failbit);
    int age;
    std::cin >> age;
    if (age < 0 || age >= 128) {
        throw WrongAgeException(age);
    }
    return age;
}

Теперь ошибка чтения в операторе >> у потока ввода будет приводить к исключению типа std::istream::failure. Функция ReadAge его не обрабатывает. Поэтому такое исключение покинет пределы этой функции. Поймаем его в функции main:

int main() {
    try {
        age = ReadAge();  // может сгенерировать исключения разных типов
        // Работаем с возрастом age
    } catch (const WrongAgeException& ex) {
        std::cerr << "Age is not correct: " << ex.age << "n";
        return 1;
    } catch (const std::istream::failure& ex) {
        std::cerr << "Failed to read age: " << ex.what() << "n";
        return 1;
    } catch (...) {
        std::cerr << "Some other exceptionn";
        return 1;
    }
    // ...
}

При обработке мы воспользовались функцией ex.what у исключения типа std::istream::failure. Такие функции есть у всех исключений стандартной библиотеки: они возвращают текстовое описание ошибки.

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

Исключения стандартной библиотеки

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

Вот несколько примеров:

  1. Функция at у контейнеров std::array, std::vector и std::deque генерирует исключение std::out_of_range при некорректном индексе.

  2. Аналогично, функция at у std::map, std::unordered_map и у соответствующих мультиконтейнеров генерирует исключение std::out_of_range при отсутствующем ключе.

  3. Обращение к значению у пустого объекта std::optional приводит к исключению std::bad_optional_access.

  4. Потоки ввода-вывода могут генерировать исключение std::ios_base::failure.

Исключения в конструкторах

В главе 3.1 мы написали класс Time. Этот класс должен был соблюдать инвариант на значение часов, минут и секунд: они должны были быть корректными. Если на вход конструктору класса Time передавались некорректные значения, мы приводили их к корректным, используя деление с остатком.

Более правильным было бы сгенерировать в конструкторе исключение. Таким образом мы бы явно передали сообщение об ошибке во внешнюю функцию, которая пыталась создать объект.

class Time {
private:
    int hours, minutes, seconds;

public:
    // Заведём класс для исключения и поместим его внутрь класса Time как в пространство имён
    class IncorrectTimeException {
    };

    Time::Time(int h, int m, int s) {
        if (s < 0 || s > 59 || m < 0 || m > 59 || h < 0 || h > 23) {
            throw IncorrectTimeException();
        }
        hours = h;
        minutes = m;
        seconds = s;
    }

    // ...
};

Генерировать исключения в конструкторах — совершенно нормальная практика. Однако не следует допускать, чтобы исключения покидали пределы деструкторов. Чтобы понять причины, посмотрим подробнее, что происходит при генерации исключения.

Свёртка стека

Вспомним класс Logger из предыдущей главы. Посмотрим, как он ведёт себя при возникновении исключения. Воспользуемся в этом примере стандартным базовым классом std::exception, чтобы не писать свой класс исключения.

#include <exception>
#include <iostream>

void f() {
    std::cout << "Welcome to f()!n";
    Logger x;
    // ...
    throw std::exception();  // в какой-то момент происходит исключение
}

int main() {
    try {
        Logger y;
        f();
    } catch (const std::exception&) {
        std::cout << "Something happened...n";
        return 1;
    }
}

Мы увидим такой вывод:

Logger(): 1
Welcome to f()!
Logger(): 2
~Logger(): 2
~Logger(): 1
Something happened...

Сначала создаётся объект y в блоке try. Затем мы входим в функцию f. В ней создаётся объект x. После этого происходит исключение. Мы должны досрочно покинуть функцию. В этот момент начинается свёртка стека (stack unwinding): вызываются деструкторы для всех созданных объектов в самой функции и в блоке try, как если бы они вышли из своей области видимости. Поэтому перед обработчиком исключения мы видим вызов деструктора объекта x, а затем — объекта y.

Аналогично, свёртка стека происходит и при генерации исключения в конструкторе. Напишем класс с полем Logger и сгенерируем нарочно исключение в его конструкторе:

#include <exception>
#include <iostream>

class C {
private:
    Logger x;

public:
    C() {
        std::cout << "C()n";
        Logger y;
        // ...
        throw std::exception();
    }

    ~C() {
        std::cout << "~C()n";
    }
};

int main() {
    try {
        C c;
    } catch (const std::exception&) {
        std::cout << "Something happened...n";
    }
}

Вывод программы:

Logger(): 1  // конструктор поля x
C()
Logger(): 2  // конструктор локальной переменной y
~Logger(): 2  // свёртка стека: деструктор y
~Logger(): 1  // свёртка стека: деструктор поля x
Something happened...

Заметим, что деструктор самого класса C не вызывается, так как объект в конструкторе не был создан.

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

Пример с динамической памятью

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

void f() {
    Logger* ptr = new Logger();  // конструируем объект класса Logger в динамической памяти
    // ...
    g();  // вызываем какую-то функцию
    // ...
    delete ptr;  // вызываем деструктор и очищаем динамическую память
}

На первый взгляд кажется, что в этом коде нет ничего опасного: delete вызывается в конце функции. Однако функция g может сгенерировать исключение. Мы не перехватываем его в нашей функции f. Механизм свёртки уберёт со стека лишь сам указатель ptr, который является автоматической переменной примитивного типа. Однако он ничего не сможет сделать с объектом в памяти, на которую ссылается этот указатель. В логе мы увидим только вызов конструктора класса Logger, но не увидим вызова деструктора. Нам придётся обработать исключение вручную:

void f() {
    Logger* ptr = new Logger();
    // ...
    try {
        g();
    } catch (...) {  // ловим любое исключение
        delete ptr;  // вручную удаляем объект
        throw;  // перекидываем объект исключения дальше
    }
    // ...
    delete ptr;

}

Здесь мы перехватываем любое исключение и частично обрабатываем его, удаляя объект в динамической памяти. Затем мы прокидываем текущий объект исключения дальше с помощью оператора throw без аргументов.

Согласитесь, этот код очень далёк от совершенства. При непосредственной работе с объектами в динамической памяти нам приходится оборачивать в try/catch любую конструкцию, из которой может вылететь исключение. Понятно, что такой код чреват ошибками. В главе 3.6 мы узнаем, как с точки зрения C++ следует работать с такими ресурсами, как память.

Гарантии безопасности исключений

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

Не вдаваясь в детали, давайте посмотрим, как могла бы выглядеть функция добавления элемента.

template <typename T>
class List {
private:
    struct Node {  // узел двусвязного списка
        T element;
        Node* prev = nullptr;  // предыдущий узел
        Node* next = nullptr;  // следующий узел
    };

    Node* first = nullptr;  // первый узел списка
    Node* last = nullptr;  // последний узел списка
    int elementsCount = 0;

public:
    // ...

    size_t Size() const {
        return elementsCount;
    }

    void PushBack(const T& elem) {
        ++elementsCount;

        // Конструируем в динамической памяти новой узел списка
        Node* node = new Node(elem, last, nullptr);

        // Связываем новый узел с остальными узлами
        if (last != nullptr) {
            last->next = node;
        } else {
            first = node;
        }
        last = node;
    }
};

Не будем здесь рассматривать другие функции класса — конструкторы, деструктор, оператор присваивания… Рассмотрим функцию PushBack. В ней могут произойти такие исключения:

  1. Выражение new может сгенерировать исключение std::bad_alloc из-за нехватки памяти.

  2. Конструктор копирования класса T может сгенерировать произвольное исключение. Этот конструктор вызывается при инициализации поля element создаваемого узла в конструкторе класса Node. В этом случае new ведёт себя как транзакция: выделенная перед этим динамическая память корректно вернётся системе.

Эти исключения не перехватываются в функции PushBack. Их может перехватить код, из которого PushBack вызывался:

#include <iostream>

class C;  // какой-то класс

int main() {
    List<C> data;
    C element;

    try {
        data.PushBack(element);
    } catch (...) {  // не получилось добавить элемент
        std::cout << data.Size() << "n";  // внезапно 1, а не 0
    }

    // работаем дальше с data
}

Наша функция PushBack сначала увеличивает счётчик элементов, а затем выполняет опасные операции. Если происходит исключение, то в классе List нарушается инвариант: значение счётчика elementsCount перестаёт соответствовать реальности. Можно сказать, что функция PushBack не даёт гарантий безопасности.

Всего выделяют четыре уровня гарантий безопасности исключений (exception safety guarantees):

  1. Гарантия отсутствия сбоев. Функции с такими гарантиями вообще не выбрасывают исключений. Примерами могут служить правильно написанные деструктор и конструктор перемещения, а также константные функции вида Size.

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

  3. Базовая гарантия безопасности. При исключении состояние объекта может поменяться, но оно останется внутренне согласованным, то есть, инварианты будут соблюдаться.

  4. Отсутсвие гарантий. Это довольно опасная категория: при возникновении исключений могут нарушаться инварианты.

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

Переместим в нашей функции PushBack изменение счётчика в конец:

    void PushBack(const T& elem) {
        Node* node = new Node(elem, last, nullptr);

        if (last != nullptr) {
            last->next = node;
        } else {
            first = node;
        }
        last = node;

        ++elementsCount;  // выполнится только если раньше не было исключений
    }

Теперь такая функция соответствует строгой гарантии безопасности.

В документации функций из классов стандартной библиотеки обычно указано, какой уровень гарантии они обеспечивают. Рассмотрим, например, гарантии безопасности класса std::vector.

  • Деструктор, функции empty, size, capacity, а также clear предоставляют гарантию отсутствия сбоев.

  • Функции push_back и resize предоставляют строгую гарантию.

  • Функция insert предоставляет лишь базовую гарантию. Можно было бы сделать так, чтобы она предоставляла строгую гарантию, но за это пришлось бы заплатить её эффективностью: при вставке в середину вектора пришлось бы делать реаллокацию.

Функции класса, которые гарантируют отсутсвие сбоев, следует помечать ключевым словом noexcept:

class C {
public:
    void f() noexcept {
        // ...
    }
};

С одной стороны, эта подсказка позволяет компилятору генерировать более эффективный код. С другой — эффективно обрабатывать объекты таких классов в стандартных контейнерах. Например, std::vector<C> при реаллокации будет использовать конструктор перемещения класса C, если он помечен как noexcept. В противном случае будет использован конструктор копирования, который может быть менее эффективен, но зато позволит обеспечить строгую гарантию безопасности при реаллокации.

Пытаюсь написать свой класс исключений

class MathException : std::exception
{
public:
    MathException(std::string &&whatStr) noexcept : whatStr(std::move(whatStr)) { }
    MathException(const std::string &whatStr) noexcept : whatStr(whatStr) { }
    ~MathException() noexcept;

    const char* what() const noexcept override;

private:
    std::string whatStr;
};

const char* MathException::what() const noexcept
{
    return whatStr.c_str();
}

int main()
try
{
    throw MathException("Parse Error");
}
catch(...)
{

}

На что мне компилятор вежливо отвечает:

error: undefined reference to 'typeinfo for MathException'                                                                                                                                
error: undefined reference to 'MathException::~MathException()'                                                                                                                           
error: undefined reference to 'vtable for MathException'                                                                                                                                             
the vtable symbol may be undefined because the class is missing its key function (see go/missingkeymethod) 

Исключительные ситуации

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

Для обработки исключений в языке С# примененяются следующие ключевые слова: try, catch, throw, finally. Перечисленные ключевые слова образуют взаимосвязанную подсистему, в которой использование одного из ключевых слов влечет за собой использование других.

Обработка исключений в языке C# реализуется на основе операторных блоков try и catch.

Описание блока try и catch

Синтаксис:

try {
Блок_кода_для_которого_выполняется
мониторинг_ошибок}
catch (ExcepTypel ехОb) 
    { Обработчик_исключений_ExcepTypel }
catch (ЕхсерТуре2 ехОb) 
    {Обработчик_исключений_ЕхсерТуре2 }

Основные системные исключения приведены в таблице 10.

Тип исключения в операторе catch должен соответствовать типу перехватываемого исключения. Неперехваченное исключение непременно приводит к досрочному прекращению выполнения программы.

Таблица
10.
Основные системные исключения

Исключение Значение
ArrayTypeMismatchException Тип сохраненного значения несовместим с типом массива
DivideByZeroException Предпринята попытка деления на ноль
IndexOutOfRangeException Индекс массива выходит за пределы диапазона
InvalidCastException Некорректное преобразование в процессе выполнения
OutOfMemoryException Вызов new был неудачным из-за недостатка памяти
Overflow/Exception Переполнение при выполнении арифметической операции
StackOverflowException Переполнение стека

Для выполнения перехвата исключений вне зависимости от их типа (перехват всех исключений) возможно использование оператора catch без параметров.

Возврат из исключения

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

С целью предотвращения этой ситуации возможно указание блока кода, который вызывается после выхода из блока try / catch, с помощью блока finally в конце последовательности try / catch.

Конструкция try/catch с блоком finally

Синтаксис:

try {
// Блок кода, выполняющий мониторинг ошибок
}
catch (ExcepTypel ех1) {
//1 Обработка исключения ExcepTypel. 
}

catch (ЕхсерТуре2 ех2) {
// 2 Обработка исключения ЕхсерТуре2 .
finally {
// Код блока finally. 
}

Блок finally будет вызываться независимо от того, появится исключение или нет, а также независимо от причин возникновения такового.

Генерация исключений

Исключения автоматически генерируются средой программирования. Однако исключение может быть и явно сгенерировано посредством оператора throw.

Оператор throw

Синтаксис:

Исключение, перехваченное одним оператором catch, может генерироваться повторно, и благодаря этому перехватываться внешним оператором catch. Для этого указывается ключевое слово throw без имени исключения.

Наследование классов исключений

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

Создаваемые пользователем классы исключений автоматически получают доступные для них свойства и методы, определенные в классе Exception.

Порядок выполнения работы

  1. Реализовать программу на языке C# в соответствии с вариантом исполнения.
  2. Графически проиллюстрировать место реализованного исключения в общей иерархии.

Варианты заданий

Реализовать обработку ошибок для лабораторной работы №9, при этом переопределив с помощью наследования событие:

  1. StackOverflowException
  2. ArrayTypeMismatchException
  3. DivideByZeroException
  4. IndexOutOfRangeException
  5. InvalidCastException
  6. OutOfMemoryException
  7. OverflowException

Цитата
Сообщение от rshelegeda
Посмотреть сообщение

Это метод, относящийся к классу Exception? Но он вынесен за пределы класса? И про слово inline — это для того, что бы программа работала быстрее?

У вас в оригинальной иерархии классов класс Exception является абстрактным, т.к. его метод GetError объявлен как pure virtual (т.е. = 0). Я не хотел менять эту деталь — раз вы захотели, чтобы Exception был абстрактным классом, то пусть так и будет.

Однако в моей реализации метод GetError получает вполне реальное определение. Синтаксис языка С++ не позволяет одновременно, прямо в определении класса объявлять метод как = 0 и тут же сразу предоставлять определение этого метода. В таких случаях, если вы хотите предоставить определение для = 0 метода, это определение приходится волей-неволей выносить за пределы класса.

Что я и сделал.

А то, что этот метод у меня явно объявлен как inline… Так и у вас в коде все методы являются inline. Методы, которые определены прямо в определении класса, являются inline даже без явного указания слова inline. Я в этом случае поставил в определение слово inline не для того «что бы программа работала быстрее», а просто для того, чтобы сохранить единообразие с тем, что уже и так написано у вас. Фактически это вы выбрали inline, а не я.

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

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

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


Содержание

  • 1. Текст демонстрационного примера для приложения типа Console Application
  • 2. Объяснение к работе программы. Схема иерархии классов
  • 3. Возможность наследования от класса exception
  • Связанные темы

Поиск на других ресурсах:

1. Текст демонстрационного примера для приложения типа Console Application

Пример демонстрирует обработку исключительных ситуаций:

  • деление на нуль;
  • отрицательный индекс в массиве.
#include <iostream>
#include <string>
using namespace std;

// Создание иерархии классов, обрабатывающих исключения
// абстрактный базовый класс, создать объект класса невозможно
class BaseException
{
protected:
  // объяснение к исключению - общее для всех производных классов
  string text;

public:
  // чистая виртуальная функция, которая выводит текст исключения
  virtual string what() = 0;
};

// класс, который соответствует арифметическим операциям
class ArithmeticException :public BaseException
{
public:
  // конструкторы класса
  // конструктор по умолчанию
  ArithmeticException()
  {
    text = "Error. Arithmetic Exception.";
  }

  // конструктор с заданным текстом
  ArithmeticException(string _text) { text = _text; }

  // переопределяем виртуальную функцию what() - обязательно
  string what() { return text; }
};

// разновидность класса ArithmeticException - деление на 0,
// поскольку класс есть внизу иерархии и из него унаследовать ничего уже
// нельзя, то объявление класса содержит спецификатор final
class DivideByZero final :public ArithmeticException
{
public:
  // конструктор, который содержит текст описывающий данное исключение
  DivideByZero() :ArithmeticException()
  {
    text = "Divide by zero."; // текст по умолчанию
  }

  // конструктор, который получает строку текста от пользователя
  DivideByZero(string _text) :ArithmeticException(_text)
  {    
  }

  string what()
  {
    return text;
  }
};

// разновидность класса BaseException
// отрицательный индекс в массиве
// это также final-класс
class NegativeIndex final :public BaseException
{
public:
  // конструктор получает аргумент по умолчанию
  NegativeIndex(string _text = "Error. Negative Index.") { text = _text; }

  // обязательно нужно переопределить функцию what()
  string what() { return text; }
};

  // Демонстрационная функция для класса NegativeIndex
void DemoExceptions1()
{
  int a[10];
  int index;

  for (int i = 0; i < 10; i++)
    a[i] = i * i;
  cout << "Input index: " << endl;
  cin >> index;

  // проверка индекса на отрицательное значение
  if (index < 0)
    throw NegativeIndex(); // сгенерировать исключение типа NegativeIndex
  cout << "a[" << index << "] = " << a[index] << endl;
}

// Демонстрационная функция для класса ArithmeticException
void DemoExceptions2()
{
  int a, b, c;
  cout << "a = "; cin >> a;
  cout << "b = "; cin >> b;

  // делить на 0 запрещено
  if (b == 0)
    throw DivideByZero("Divide by 0.");
  cout << "a / b = " << (double)a / b << endl;
}

void main()
{
  try
  {
    // вызвать функцию, генерирующую исключение
    DemoExceptions1(); // проверка на "Negative Index"
    DemoExceptions2(); // проверка на "Divide by 0"
    cout << "OK!" << endl;
  }
  catch (NegativeIndex e)
  {
    // обработать исключение
    cout << e.what() << endl;
  }
  catch (DivideByZero e)
  {
    cout << e.what() << endl;
  }
}

 



2. Объяснение к работе программы. Схема иерархии классов

В примере создается абстрактный базовый класс BaseException в котором объявляются:

  • строка text типа string, которая есть объяснением к исключительной ситуации;
  • чистая виртуальная функция what() предназначенная для вывода строки text на экран.

Из класса BaseException наследуется класс ArithmeticException, предназначенный для обработки арифметических исключительных ситуаций. Из класса ArithmeticException наследуются два класса:

  • класс DivideByZero – предназначен для обработки исключительной ситуации деления на ноль;
  • класс NegativeIndex – предназначен для обработки исключительной ситуации отрицательного индекса из массива.

Общая схема реализованных классов изображена на рисунке.

C++. Схема реализованных классов исключений

 

3. Возможность наследования от класса exception

В примере объявляется собственный базовый класс BaseException. Рекомендуется собственные классы обработки исключительных ситуаций наследовать от класса exception библиотеки C++. То есть, более правильно было бы объявить класс BaseException следующим образом:

// класс BaseException унаследован от класса exception
class BaseException: public exception
{
protected:
  string text; // объяснение к исключению - общее для всех производных классов

public:
  virtual string what() = 0; // чистая виртуальная функция, которая выводит текст исключения
};

 


Связанные темы

  • Понятие исключительной ситуации. Инструкция try…catch. Оператор throw. Примеры использования

 


Программирование: теория и практика

0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии

А вот еще интересные материалы:

  • Яшка сломя голову остановился исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного исправьте ошибки
  • Ясность цели позволяет целеустремленно добиваться намеченного где ошибка
  • Каким образом проводится экстраполяция ошибки выборки на генеральную совокупность
  • Каким образом можно уменьшить влияние ошибок восприятия на общение кратко