Cpp runtime error

Defines a type of object to be thrown as exception. It reports errors that are due to events beyond the scope of the program and can not be easily predicted.

Defines a type of object to be thrown as exception. It reports errors that are due to events beyond the scope of the program and can not be easily predicted.

Exceptions of type std::runtime_error are thrown by the following standard library components: std::locale::locale and std::locale::combine.

In addition, the following standard exception types are derived from std::runtime_error:

  • std::range_error
  • std::overflow_error
  • std::underflow_error
  • std::chrono::ambiguous_local_time
  • std::chrono::nonexistent_local_time
  • std::format_error
(since C++20)

std-runtime error-inheritance.svg

Inheritance diagram

Contents

  • 1 Member functions
  • 2 std::runtime_error::runtime_error
    • 2.1 Parameters
    • 2.2 Exceptions
    • 2.3 Notes
  • 3 std::runtime_error::operator=
    • 3.1 Parameters
    • 3.2 Return value
    • 3.3 Notes
  • 4 Inherited from std::exception
    • 4.1 Member functions
    • 4.2 Defect reports

[edit] Member functions

constructs a new runtime_error object with the given message
(public member function)
replaces the runtime_error object
(public member function)

std::runtime_error::runtime_error

runtime_error( const std::string& what_arg );

(1)

runtime_error( const char* what_arg );

(2)
(3)

runtime_error( const runtime_error& other );

(until C++11)

runtime_error( const runtime_error& other ) noexcept;

(since C++11)

1) Constructs the exception object with what_arg as explanatory string. After construction, std::strcmp(what(), what_arg.c_str()) == 0.

2) Constructs the exception object with what_arg as explanatory string. After construction, std::strcmp(what(), what_arg) == 0.

3) Copy constructor. If *this and other both have dynamic type std::runtime_error then std::strcmp(what(), other.what()) == 0. No exception can be thrown from the copy constructor. (until C++11)

Parameters

what_arg explanatory string
other another exception object to copy

Exceptions

Notes

Because copying std::runtime_error is not permitted to throw exceptions, this message is typically stored internally as a separately-allocated reference-counted string. This is also why there is no constructor taking std::string&&: it would have to copy the content anyway.

Before the resolution of LWG issue 254, the non-copy constructor can only accept std::string. It makes dynamic allocation mandatory in order to construct a std::string object.

After the resolution of LWG issue 471, a derived standard exception class must have a publicly accessible copy constructor. It can be implicitly defined as long as the explanatory strings obtained by what() are the same for the original object and the copied object.

std::runtime_error::operator=

runtime_error& operator=( const runtime_error& other );

(until C++11)

runtime_error& operator=( const runtime_error& other ) noexcept;

(since C++11)

Assigns the contents with those of other. If *this and other both have dynamic type std::runtime_error then std::strcmp(what(), other.what()) == 0 after assignment. No exception can be thrown from the copy assignment operator. (until C++11)

Parameters

other another exception object to assign with

Return value

*this

Notes

After the resolution of LWG issue 471, a derived standard exception class must have a publicly accessible copy assignment operator. It can be implicitly defined as long as the explanatory strings obtained by what() are the same for the original object and the copied object.

Inherited from std::exception

Member functions

destroys the exception object
(virtual public member function of std::exception) [edit]
returns an explanatory string
(virtual public member function of std::exception) [edit]

[edit] Defect reports

The following behavior-changing defect reports were applied retroactively to previously published C++ standards.

DR Applied to Behavior as published Correct behavior
LWG 254 C++98 the constructor accepting const char* was missing added
LWG 471 C++98 the explanatory strings of std::runtime_error‘s
copies were implementation-defined
they are the same as that of the
original std::runtime_error object

Improve Article

Save Article

  • Read
  • Discuss
  • Improve Article

    Save Article

    In this article, we will discuss the reason for the run-time error and its solution. Runtime Error: A runtime error in a program is an error that occurs while the program is running after being successfully compiled. Below are some methods to identify the reason behind Runtime errors: 

    Method 1: When the index of the array is assigned with a negative index it leads to invalid memory access during runtime error. Below is the C++ Program to illustrate the invalid memory access during run-time: 

    C++

    #include <iostream>

    using namespace std;

    int arr[5];

    int main()

    {

        int answer = arr[-10];

        cout << answer;

        return 0;

    }

    Method 2: Sometimes Array or vector runs out of bounds of their limits resulting in a runtime error. Below is the C++ program illustrating array runs out of bound:

    C++

    #include <iostream>

    using namespace std;

    int main()

    {

        long n;

        n = 100000000000;

        long a[n];

        cout << a[1] << " ";

        return 0;

    }

    Output:

    Explanation:

    • This is an error for index out of bound.
    • It can be resolved by using the size of the array/vector as within the limit.

    Method 3: Some silly mistakes like dividing by zero encountered while coding in a hurry, sometimes leads to runtime error. Below is the C++ program illustrating runtime error by dividing by zero and un-assigned variables:

    C++

    #include <iostream>

    using namespace std;

    int main() {

          int n = 0;

        cout << 5/n;

        return 0;

    }

    Output: 

    C++

    #include <iostream>

    using namespace std;

    int main()

    {

        long long N;

        long arr[N];

        cin >> N;

        for (int i = 0; i < N; i++) {

            cin >> arr[i];

        }

        for (int i = 0; i < N; i++) {

            cout << arr[i] << " ";

        }

        return 0;

    }

    Output:

    Explanation:

    The above program shows “Bad memory access (SIGBUS)” because:

    • Here, variable N is assigned a garbage value resulting in a runtime error.
    • Sometimes, it depends on how the compiler assigned the garbage value.
    • This can be resolved by declaring arr[N] after scanning the value for variable n and checking if it is the upper or lower limit of the array/vector index.

    Permalink

    Cannot retrieve contributors at this time

    description title ms.date f1_keywords helpviewer_keywords ms.assetid

    Learn more about: runtime_error Class

    runtime_error Class

    09/09/2021

    stdexcept/std::runtime_error

    runtime_error class

    4d0227bf-847b-45a2-a320-2351ebf98368

    runtime_error Class

    The class serves as the base class for all exceptions thrown to report errors presumably detectable only when the program executes.

    Syntax

    class runtime_error : public exception {
    public:
        explicit runtime_error(const string& message);
    
        explicit runtime_error(const char *message);
    
    };

    Remarks

    The value returned by what() is a copy of message.data(). For more information, see what and data.

    Example

    // runtime_error.cpp
    // compile with: /EHsc
    #include <exception>
    #include <iostream>
    #include <locale>
    #include <typeinfo>
    using namespace std;
    
    int main()
    {
       try
       {
          locale loc("test");
       }
       catch (const exception& e)
       {
          cerr << "Caught: " << e.what() << endl;
          cerr << "Type: " << typeid(e).name() << endl;
       }
    }
    /* Output:
    Caught: bad locale name
    Type: class std::runtime_error
    */

    Requirements

    Header: <stdexcept>

    Namespace: std

    See also

    exception Class
    Thread Safety in the C++ Standard Library

    1. 1. Инварианты
    2. 2. Виды исключений
    3. 3. std::logic_error
    4. 4. std::invalid_argument
    5. 5. std::domain_error
    6. 6. std::length_error
    7. 7. std::out_of_range
    8. 8. std::future_error
    9. 9. std::runtime_error
    10. 10. std::range_error
    11. 11. std::overflow_error
    12. 12. std::underflow_error
    13. 13. std::system_error
    14. 14. std::ios_base::failure
    15. 15. std::bad_typeid
    16. 16. std::bad_cast
    17. 17. std::bad_weak_ptr
    18. 18. std::bad_function_call
    19. 19. std::bad_alloc
    20. 20. std::bad_array_new_length
    21. 21. std::bad_exception

    Что такое исключение? Это ситуация, которая не предусмотрена стандартным поведением программы. Например, попытка доступа к элементу в классе Vector (который мы разбирали в статье про

    классы

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

    • Как правило в таких случаях, автор класса

      Vector

      не знает, как пользователь захочет использовать его класс, а также не знает в какой программе этот класс будет использоваться.
    • Пользователь класса

      Vector

      не может всегда контролировать правильность работы этого класса, поэтому ему нужно сообщить о том, что что-то пошло не так.

    Для разрешения таких ситуация в C++ можно использовать технику исключений.


    Рассмотрим, как написать вызов исключения в случае попытки доступа к элементу по индексу, который не существует в классе Vector.

    double& Vector::operator[](int i)
    {
        if (i<0 || size()<=i) throw out_of_range{"Vector::operator[]"};
        return elem[i];
    }
    

    Здесь применяется исключение

    out_of_range.

    Данное исключение определено в заголовочном файле

    .

    Оператор

    throw

    передаёт контроль обработчику для исключений типа

    out_of_range

    в некоторой функции, которая прямо или косвенно вызывает

    Vector::operator

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

    try catch.

    void f(Vector& v)
    {
        // ...
        try { // блок обработки функции с исключением
            v[v.size()] = 7; // попытка доступа к элементу за пределами вектора
        }
        catch (out_of_range) { // ловим ошибку out_of_range 
            // ... обработки ошибки out_of_range ...
        }
        // ...
    }
    

    Инварианты

    Также блоки

    try catch

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

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

    Vector::Vector(int s)
    {
        if (s < 0) throw length_error{};
        elem = new double[s];
        sz = s;
    }
    

    Данный конструктор может выбросить исключение в двух случаях:

    • Если в качестве аргумента

      size

      будет передано отрицательное значение
    • Если оператор

      new

      не сможет выделить память


    length_error

    — это стандартный оператор исключений, поскольку библиотека std часто использует данные исключения при своей работе.

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

    void test()
    {
        try {
            Vector v(−27);
        }
        catch (std::length_error) {
            // обработка отрицательного размера вектора
        }
        catch (std::bad_alloc) {
            // обработка ошибки выделения памяти
        }
    }
    

    Также можно выделить свои собственные исключения.

    Виды исключений

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

    std::exception.

    На данный момент существуют следующие виды исключений:

    • logic_error
    • invalid_argument
    • domain_error
    • length_error
    • out_of_range
    • future_error (C++11)
    • runtime_error
    • range_error
    • overflow_error
    • underflow_error
    • system_error (C++11)
    • ios_base::failure (начиная с C++11)
    • bad_typeid
    • bad_cast
    • bad_weak_ptr (C++11)
    • bad_function_call (C++11)
    • bad_alloc
    • bad_array_new_length (C++11)
    • bad_exception
    • ios_base::failure (до C++11)

    std::logic_error

    Исключение определено в заголовочном файле

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

    Этот класс используется как основа для ошибок, которые могут быть определены только во время выполнения программы.

    std::invalid_argument

    Исключение определено в заголовочном файле

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

    Например, на MSDN приведён пример, когда в объект класса bitset из стандартной библиотеки

    // invalid_arg.cpp  
    // compile with: /EHsc /GR  
    #include <bitset>  
    #include <iostream>  
    
    using namespace std;  
    
    int main( )  
    {  
       try   
       {  
          bitset< 32 > bitset( string( "11001010101100001b100101010110000") );  
       }  
       catch ( exception &e )   
       {  
          cerr << "Caught " << e.what( ) << endl;  
          cerr << "Type " << typeid( e ).name( ) << endl;  
       };  
    }  
    * Output:   
    Caught invalid bitset<N> char  
    Type class std::invalid_argument  
    * 
    

    В данном примере передаётся неправильная строка, внутри которой имеется символ ‘b’, который будет ошибочным.

    std::domain_error

    Исключение определено в заголовочном файле

    Наследован от std::logic_error. Определяет исключение, которое должно быть брошено в случае если математическая функция не определена для того аргумента, который ей передаётся, например:

    std::sqrt(-1)
    

    std::length_error

    Исключение определено в заголовочном файле

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

    std::out_of_range

    Исключение определено в заголовочном файле

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

    std::future_error

    Исключение определено в заголовочном файле

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

    std::error_code

    .

    std::runtime_error

    Исключение определено в заголовочном файле

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

    std::range_error

    Исключение определено в заголовочном файле

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

    underflow_error

    или

    overflow_error

    .

    std::overflow_error

    Исключение определено в заголовочном файле

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

    std::underflow_error

    Исключение определено в заголовочном файле

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

    std::system_error

    Исключение определено в заголовочном файле


    std::system_error

    — это тип исключения, которое вызывается различными функциями стандартной библиотеки (как правило, функции, которые взаимодействуют с операционной системой, например, конструктор

    std::thread

    ), при этом исключение имеет соответствующий

    std::error_code

    .

    std::ios_base::failure

    Исключение определено в заголовочном файле

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

    std::bad_typeid

    Исключение определено в заголовочном файле

    Исключение этого типа возникает, когда оператор

    typeid

    применяется к нулевому указателю полиморфного типа.

    #include <iostream>
    #include <typeinfo>
    
    struct S { // Тип должен быть полиморфным
        virtual void f();
    }; 
    
    int main()
    {
        S* p = nullptr;
        try {
            std::cout << typeid(*p).name() << 'n';
        } catch(const std::bad_typeid& e) {
            std::cout << e.what() << 'n';
        }
    }
    

    std::bad_cast

    Исключение определено в заголовочном файле

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

    #include <iostream>
    #include <typeinfo>
    
    struct Foo { virtual ~Foo() {} };
    struct Bar { virtual ~Bar() {} };
    
    int main()
    {
        Bar b;
        try {
            Foo& f = dynamic_cast<Foo&>(b);
        } catch(const std::bad_cast& e)
        {
            std::cout << e.what() << 'n';
        }
    }
    

    std::bad_weak_ptr

    Исключение определено в заголовочном файле


    std::bad_weak_ptr

    – тип объекта, генерируемый в качестве исключения конструкторами

    std::shared_ptr

    , которые принимают

    std::weak_ptr

    в качестве аргумента, когда

    std::weak_ptr

    ссылается на уже удаленный объект.

    #include <memory>
    #include <iostream>
    int main()
    {
        std::shared_ptr<int> p1(new int(42));
        std::weak_ptr<int> wp(p1);
        p1.reset();
        try {
            std::shared_ptr<int> p2(wp);
        } catch(const std::bad_weak_ptr& e) {
            std::cout << e.what() << 'n';
        }
    }
    

    std::bad_function_call

    Исключение определено в заголовочном файле

    Данное исключение генерируется в том случае, если был вызван метод

    std::function::operator()

    объекта

    std::function

    , который не получил объекта функции, то есть ему был передан в качестве инициализатора nullptr, например, а объект функции так и не был передан.

    #include <iostream>
    #include <functional>
    
    int main()
    {
        std::function<int()> f = nullptr;
        try {
            f();
        } catch(const std::bad_function_call& e) {
            std::cout << e.what() << 'n';
        }
    }
    

    std::bad_alloc

    Исключение определено в заголовочном файле

    Вызывается в том случае, когда не удаётся выделить память.

    std::bad_array_new_length

    Исключение определено в заголовочном файле

    Исключение вызывается в следующих случаях:

    1. Массив имеет отрицательный размер
    2. Общий размер нового массива превысил максимальное значение, определяемое реализацией
    3. Количество элементов инициализации превышает предлагаемое количество инициализирующих элементов
    #include <iostream>
    #include <new>
    #include <climits>
    
    int main()
    {
        int negative = -1;
        int small = 1;
        int large = INT_MAX;
        try {
            new int[negative];           // negative size
            new int[small]{1,2,3};       // too many initializers
            new int[large][1000000];     // too large
        } catch(const std::bad_array_new_length &e) {
            std::cout << e.what() << 'n';
        }
    }
    

    std::bad_exception

    Исключение определено в заголовочном файле


    std::bad_exception

    — это тип исключения в C++, которое выполняется в следующих ситуациях:

    1. Если нарушается динамическая спецификация исключений
    2. Если

      std::exception_ptr

      хранит копию пойманного исключения, и если конструктор копирования объекта исключения поймал current_exception, тогда генерируется исключение захваченных исключений.
    #include <iostream>
    #include <exception>
    #include <stdexcept>
    
    void my_unexp() { throw; }
    
    void test() throw(std::bad_exception)
    {
        throw std::runtime_error("test");
    }
    
    int main()
    {
        std::set_unexpected(my_unexp);
        try {
             test();
        } catch(const std::bad_exception& e)
        {
            std::cerr << "Caught " << e.what() << 'n';
        }
    }
    

    Поговорим об исключениях в C++, начиная определением и заканчивая грамотной обработкой.

    1. Инструмент программирования для исключительных ситуаций
    2. Исключения: панацея или нет
    3. Синтаксис исключений в C++
    4. Базовые исключения стандартной библиотеки
    5. Заключение

    Георгий Осипов

    Георгий Осипов


    Один из авторов курса «Разработчик C++» в Яндекс Практикуме, разработчик в Лаборатории компьютерной графики и мультимедиа ВМК МГУ

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

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

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

    В первой части разберёмся:

    • для чего нужны исключения;
    • особенности C++;
    • синтаксис выбрасывания и обработки исключений;
    • особые случаи, связанные с исключениями.

    Также рассмотрим основные стандартные типы исключений, где и для чего они применяются.

    Мы опираемся на современные компиляторы и Стандарт C++20. Немного затронем C++23 и даже C++03.

    Если вы только осваиваете C++, возможно, вам будет интересен курс «Разработчик C++» в Яндекс Практикуме. У курса есть бесплатная вводная часть. Именно она может стать вашим первым шагом в мир C++. Для тех, кто знаком с программированием, есть внушительная ознакомительная часть, тоже бесплатная.

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

    В жизни любой программы бывают моменты, когда всё идёт не совсем так, как задумывал разработчик. Например:

    • в системе закончилась оперативная память;
    • соединение с сервером внезапно прервалось;
    • пользователь выдернул флешку во время чтения или записи файла;
    • понадобилось получить первый элемент списка, который оказался пустым;
    • формат файла не такой, как ожидалось.

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

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

    На помощь программисту приходят исключения (exception). Так называют объекты, которые хранят данные о возникшей проблеме. Механизмы исключений в разных языках программирования очень похожи. В зависимости от терминологии языка исключения либо выбрасывают (throw), либо генерируют (raise). Это происходит в тот момент, когда программа не может продолжать выполнять запрошенную операцию.

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

    Если обработчик всё же найден, то он ловит (catch) исключение и программа продолжает работать как обычно. В некоторых языках вместо catch используется глагол except (исключить).

    Обработчик ловит не все исключения, а только некоторые — те, что возникли в конкретной части определённой функции. Эту часть нужно явно обозначить, для чего используют конструкцию try (попробовать). Также обработчик не поймает исключение, которое ранее попало в другой обработчик. После обработки исключения программа продолжает выполнение как ни в чём не бывало.

    Исключения: панацея или нет

    Перед тем как совершить операцию, нужно убедиться, что она корректна. Если да — совершить эту операцию, а если нет — выбросить исключение. Так делается в некоторых языках, но не в C++. Проверка корректности — это время, а время, как известно, деньги. В C++ считается, что программист знает, что делает, и не нуждается в дополнительных проверках. Это одна из причин, почему программы на C++ такие быстрые.

    Но за всё нужно платить. Если вы не уследили и сделали недопустимую операцию, то в менее производительных языках вы получите исключение, а в C++ — неопределённое поведение. Исключение можно обработать и продолжить выполнение программы. Неопределённое поведение гарантированно обработать нельзя.

    Но некоторые виды неопределённого поведения вполне понятны и даже могут быть обработаны. Это зависит от операционной системы:

    • сигналы POSIX — низкоуровневые уведомления, которые отправляются программе при совершении некорректных операций и в некоторых других случаях;
    • структурированные исключения Windows (SEH) — специальные исключения, которые нельзя обработать средствами языка.

    Особенность C++ в том, что не любая ошибка влечёт исключение, и не любую ошибку можно обработать. Но если для операции производительность не так критична, почему бы не сделать проверку?

    У ряда операций в C++ есть две реализации. Одна супербыстрая, но вы будете отвечать за корректность, а вторая делает проверку и выбрасывает исключение в случае ошибки. Например, к элементу класса std::vector можно обратиться двумя способами:

    • vec[15] — ничего не проверяет. Если в векторе нет элемента с индексом 15, вы получаете неопределённое поведение. Это может быть сигнал SIGSEGV, некорректное значение или взрыв компьютера.
    • vec.at(15) — то же самое, но в случае ошибки выбрасывается исключение, которое можно обработать.

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

    Ловим исключения

    Начнём с примера:

    void SomeFunction() {
        DoSomething0();
    
        try {
            SomeClass var;
    
            DoSomething1();
            DoSomething2();
    
            // ещё код
    
            cout << "Если возникло исключение, то этот текст не будет напечатан" << std::endl;
        }
        catch(ExceptionType e) {
            std::cout << "Поймано исключение: " << e.what() << std::endl;
            // ещё код
        }
    
        std::cout << "Это сообщение не будет выведено, если возникло исключение в DoSomething0 или "
                      "непойманное исключение внутри блока try." << std::endl;
    }
    

    В примере есть один try-блок и один catch-блок. Если в блоке try возникает исключение типа ExceptionType, то выполнение блока заканчивается. При этом корректно удаляются созданные объекты — в данном случае переменная var. Затем управление переходит в конструкцию catch. Сам объект исключения передаётся в переменную e. Выводя e.what(), мы предполагаем, что у типа ExceptionType есть метод what.

    Если в блоке try возникло исключение другого типа, то управление также прервётся, но поиск обработчика будет выполняться за пределами функции SomeFunction — выше по стеку вызовов. Это также касается любых исключений, возникших вне try-блока.

    Во всех случаях объект var будет корректно удалён.

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

    Ловим исключения нескольких типов

    Можно указать несколько блоков catch, чтобы обработать исключения разных типов:

    void SomeFunction() {
        DoSomething0();
    
        try {
            DoSomething1();
            DoSomething2();
            // ещё код
        }
        catch(ExceptionType1 e) {
            std::cout << "Some exception of type ExceptionType1: " << e.what() << std::endl;
            // ещё код
        }
        catch(ExceptionType2 e) {
            std::cout << "Some exception of type ExceptionType2: " << e.what() << std::endl;
            // ещё код
        }
        // ещё код
    }

    Ловим все исключения

    void SomeFunction() {
        DoSomething0();
    
        try {
            DoSomething1();
            DoSomething2();
            // ещё код
        }
        catch(...) {
            std::cout << "An exception any type" << std::endl;
            // ещё код
        }
        // ещё код
    }

    Если перед catch(...) есть другие блоки, то он означает «поймать все остальные исключения». Ставить другие catch-блоки после catch(...) не имеет смысла.

    Перебрасываем исключение

    Внутри catch(...) нельзя напрямую обратиться к объекту-исключению. Но можно перебросить тот же объект, чтобы его поймал другой обработчик:

    void SomeFunction() {
        DoSomething0();
    
        try {
            DoSomething1();
            DoSomething2();
            // ещё код
        }
        catch(...) {
            std::cout << "Какое-то исключение неизвестного типа. Сейчас не можем его обработать" << std::endl;
            throw; // перебрасываем исключение
        }
        // ещё код
    }

    Можно использовать throw в catch-блоках с указанным типом исключения. Но если поместить throw вне блока catch, то программа тут же аварийно завершит работу через вызов std::terminate().

    Перебросить исключение можно другим способом:

    std::rethrow_exception(std::current_exception())

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

    Принимаем исключение по ссылке

    Чтобы избежать лишних копирований, можно ловить исключение по ссылке или константной ссылке:

    void SomeFunction() {
        DoSomething0();
    
        try {
            DoSomething1();
            DoSomething2();
            // ещё код
        }
        catch(ExceptionType& e) {
            std::cout << "Some exception of type ExceptionType: " << e.what() << std::endl;
            // ещё код
        }
        catch(const OtherExceptionType& e) {
            std::cout << "Some exception of type OtherExceptionType: " << e.what() << std::endl;
            // ещё код
        }
    }

    Это особенно полезно, когда мы ловим исключение по базовому типу.

    Выбрасываем исключения

    Чтобы поймать исключение, нужно его вначале выбросить. Для этого применяется throw.

    Если throw используется с параметром, то он не перебрасывает исключение, а выбрасывает новое. Параметр может быть любого типа, даже примитивного. Использовать такую конструкцию разрешается в любом месте программы:

    void ThrowIfNegative(int x) {
        if (x < 0) {
            // выбрасываем исключение типа int
            throw x;
        }
    }
    
    int main() {
        try {
            ThrowIfNegative(10);
            ThrowIfNegative(-15);
            ThrowIfNegative(0);
            cout << "Этот текст никогда не будет напечатан" << std::endl;
        }
        // ловим выброшенное исключение
        catch(int x) {
            cout << "Поймано исключение типа int, содержащее число " << x << std::endl;
        }
    }

    Вывод: «Поймано исключение типа int, содержащее число –15».

    Создаём типы для исключений

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

    class IsZeroException{};
    struct IsNegativeException{};
    
    void ThrowIfNegative(int x) {
        if (x < 0) {
            // Выбрасывается не тип, а объект.
            // Не забываем скобки, чтобы создать объект заданного типа:
            throw IsNegativeException();
        }
    }
    
    void ThrowIfZero(int x) {
        if (x == 0) {
            throw IsZeroException();
        }
    }
    
    void ThrowIfNegativeOrZero(int x) {
        ThrowIfNegative(x);
        ThrowIfZero(x);
    }
    
    int main() {
        try {
            ThrowIfNegativeOrZero(10);
            ThrowIfNegativeOrZero(-15);
            ThrowIfNegativeOrZero(0);
        }
        catch(IsNegativeException x) {
            cout << "Найдено отрицательное число" << std::endl;
        }
        catch(IsZeroException x) {
            cout << "Найдено нулевое число" << std::endl;
        }
    }

    В итоге будет напечатана только фраза: «Найдено отрицательное число», поскольку –15 проверено раньше нуля.

    Ловим исключение по базовому типу

    Чтобы поймать исключение, тип обработчика должен в точности совпадать с типом исключения. Например, нельзя поймать исключение типа int обработчиком типа unsigned int.

    Но есть ситуации, в которых типы могут не совпадать. Про одну уже сказано выше: можно ловить исключение по ссылке. Есть ещё одна возможность — ловить исключение по базовому типу.

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

    class NumericException {
    public:
        virtual std::string_view what() const = 0;
    }
    
    // Класс — наследник NumericException.
    class IsZeroException : public NumericException {
    public:
        std::string_view what() const override {
            return "Обнаружен ноль";
        }
    }
    
    // Ещё один наследник NumericException.
    class IsNegativeException : public NumericException {
    public:
        std::string_view what() const override {
            return "Обнаружено отрицательное число";
        }
    }
    
    void ThrowIfNegative(int x) {
        if (x < 0) {
            // Выбрасывается не тип, а объект.
            // Не забываем скобки, чтобы создать объект заданного типа:
            throw IsNegativeException();
        }
    }
    
    void ThrowIfZero(int x) {
        if (x == 0) {
            throw IsZeroException();
        }
    }
    
    void ThrowIfNegativeOrZero(int x) {
        ThrowIfNegative(x);
        ThrowIfZero(x);
    }
    
    int main() {
        try {
            ThrowIfNegativeOrZero(10);
            ThrowIfNegativeOrZero(-15);
            ThrowIfNegativeOrZero(0);
        }
        // Принимаем исключение базового типа по константной ссылке (&):
        catch(const NumericException& e) {
            std::cout << e.what() << std::endl;
        }
    }

    Выбрасываем исключение в тернарной операции ?:

    Напомню, что тернарная операция ?: позволяет выбрать из двух альтернатив в зависимости от условия:

    std::cout << (age >= 18 ? "Проходите" : "Извините, вход в бар с 18 лет") << std::endl;

    Оператор throw можно использовать внутри тернарной операции в качестве одного из альтернативных значений. Например, так можно реализовать безопасное деление:

    int result = y != 0 ? x / y : throw IsZeroException();

    Это эквивалентно такой записи:

    int result;
    if (y != 0) {
        result = x / y;
    } 
    else {
        throw IsZeroException();
    }

    Согласитесь, первый вариант лаконичнее. Так можно выбрасывать несколько исключений в одном выражении:

    // Вычислим корень отношения чисел:
    int result = y == 0 ? throw IsZeroException() : x / y < 0 ? throw IsNegativeException() : sqrt(x / y);

    Вся функция — try-блок

    Блок try может быть всем телом функции:

    int SomeFunction(int x) try {
        return DoSomething(x);
    }
    catch(ExceptionType e) {
        std::cout << "Some exception of type ExceptionType: " << e.what() << std::endl;
        // ещё код
    
        // Для того, кто вызвал функцию, всё прошло штатно: исключение поймано.
        // Мы должны возвратить значение:
        return –1; 
    }

    Тут мы просто опустили фигурные скобки функции. По-другому можно записать так:

    int SomeFunction(int x) {
        try {
            return DoSomething(x);
        }
        catch(ExceptionType e) {
            std::cout << "Some exception of type ExceptionType: " << e.what() << std::endl;
            // ещё код
        
            // Для того, кто вызвал функцию, всё прошло штатно: исключение поймано.
            // Мы должны возвратить значение:
            return –1; 
        }
    }

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

    Есть как минимум два случая возникновения исключений в конструкторе объекта:

    1.  Внутри тела конструктора.
    2. При конструировании данных объекта.

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

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

    class IsZeroException{};
    
    // Функция выбросит исключение типа IsZeroException
    // если аргумент равен нулю.
    void ThrowIf0(int x) {
        if (x == 0) {
            throw IsZeroException();
        }
    }
    
    // Класс содержит только одно число.
    // Он выбрасывает исключение в конструкторе, если число нулевое.
    class NotNullInt {
    public:
        NotNullInt(int x) : x_(x) {
            ThrowIf0(x_);
        }
    
    private:
        int x_;
    }
    
    class Ratio {
    public:
        // Инициализаторы пишем после try:
        Ratio(int x, int y) try : x_(x), y_(y) {
        }
        catch(IsZeroException e) {
            std::cout << "Знаменатель дроби не может быть нулём" << std::endl;
            // Тут неявный throw; — конструктор прерван
        }
    
    private:
        int x_;
        NotNullInt y_;
    };
    
    int main() {
        Ratio(10, 15);
        try {
            Ratio(15, 0);
        }
        catch(...) {
            std::cout << "Дробь не построена" << std::endl;
        }
    }

    Тут мы увидим оба сообщения: «Знаменатель дроби не может быть нулём» и «Дробь не построена».

    Если объект недоконструирован, то его деструктор не вызывается. Это логичная, но неочевидная особенность языка. Однако все полностью построенные члены – данные объекта будут корректно удалены:

    #include 
    
    class A{
    public:
        A() {
            std::cout << "A constructed" << std::endl;
        }
        ~A() {
            std::cout << "A destructed" << std::endl;
        }
    private:
    }
    
    class B{
    public:
        B() {
            std::cout << "B constructed" << std::endl;
            throw 1;
        }
        ~B() {
            // Этой надписи мы не увидим:
            std::cout << "B destructed" << std::endl;
        }
        
    private:
        A a;
    };
    
    int main() {
        try {
            B b;
        }
        catch (...) {
        }
    }

    Запустим код и увидим такой вывод:

    A constructed
    B constructed
    A destructed

    Объект типа A создался и удалился, а объект типа B создался не до конца и поэтому не удалился.

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

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

    В этом разделе примера не будет, потому что исключения в деструкторе — нежелательная практика. Бывает, что язык удаляет объекты вынужденно, например, при поиске обработчика выброшенного исключения. Если во время этого возникнет другое исключение в деструкторе какого-то объекта, то это приведёт к вызову std::terminate.

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

    Обрабатываем непойманные исключения

    Поговорка «не пойман — не вор» для исключений не работает. Непойманные исключения приводят к завершению программы через std::terminate. Это нештатная ситуация, но можно предотвратить немедленное завершение, добавив обработчик для std::terminate:

    int main() {
        // Запишем обработчик в переменную terminate_handler
        auto terminate_handler = []() {
            auto e_ptr = std::current_exception();
            if (e_ptr) {
                try {
                    // Перебросим исключение:
                    std::rethrow_exception(e_ptr);
                } catch (const SomeType& e) {
                    std::cerr << "Непойманное исключение типа SomeType: " << e.what() << std::endl;
                } 
                catch (...) {
                    std::cerr << "Непойманное исключение неизвестного типа" << std::endl;
                }
            }
            else {
                std::cerr << "Неизвестная ошибка" << std::endl;
            }
    
            // Всё равно завершим программу.
            std::abort();
        };
        
        // Установим обработчик для функции terminate
        std::set_terminate(terminate_handler);
    
        // …..
    }

    Однако не стоит надеяться, что программа после обработки такой неприятной ситуации продолжит работу как ни в чём не бывало. std::terminate — часть завершающего процесса программы. Внутри него доступен только ограниченный набор операций, зависящий от операционной системы.

    Остаётся только сохранить всё, что можно, и извиниться перед пользователем за неполадку. А затем выйти из программы окончательно вызовом std::abort().

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

    Далеко не всегда есть смысл создавать новый тип исключений, ведь в стандартной библиотеке их и так немало. А если вы всё же создаёте свои исключения, то сделайте их наследниками одного из базовых. Рекомендуется делать все типы исключений прямыми или косвенными наследниками std::exception.

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

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

    #include 
    #include 
    
    int main() {
        // Используем std::runtime_error вместо std::string.
        // Но зачем?
        std::runtime_error err("Буря мглою небо кроет");
    
        std::cout << err.what() << std::endl;
    }

    Разберём основные типы исключений, описанные в стандартной библиотеке C++.

    std::exception

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

    std::logic_error : public std::exception

    Исключение типа logic_error выбрасывается, когда нарушены условия, сформулированные на этапе написания программы. Например, мы передали в функцию извлечения квадратного корня отрицательное число или попытались извлечь элемент из пустого списка.

    Конструктор принимает сообщение в виде std::string, которое будет возвращаться методом what().

    // класс копилка
    class Moneybox {
    public:
        void WithdrawCoin() {
            if (coins_ == 0) {
                throw std::logic_error("В копилке нет денег");
            }
            --coins_;
        }
        void PutCoin() {
            ++coins_;
        }
    
    private:
        int coins_ = 0;
    }

    Перечислим некоторые производные классы std::logic_error. У всех них похожий интерфейс.

    • std::invalid_argument. Исключение этого типа показывает, что функции передан некорректный аргумент, не соответствующий условиям.
    double GetSqrt(double x) {
        return x >= 0 ? sqrt(x) : 
            throw std::invalid_argument("Попытка извлечь квадратный корень из отрицательного числа");
    }

    Это исключение выбрасывают функции преобразования строки в число, такие как stol, stof, stoul, а также конструктор класса std::bitset:

    try {
        int f = std::stoi("abracadabra");
    } catch (std::invalid_argument& ex) {
        std::cout << ex.what() << 'n';
    }
    • std::length_error. Исключение говорит о том, что превышен лимит вместимости контейнера. Может выбрасываться из методов, меняющих размер контейнеров string и vector. Например resize, reserve, push_back.
    • std::out_of_range. Исключение говорит о том, что некоторое значение находится за пределами допустимого диапазона. Возникает при использовании метода at практически всех контейнеров. Также возникает при использовании функций конвертации в строки в число, таких как stol, stof, stoul. В стандартной библиотеке есть исключение с похожим смыслом — std::range_error.

    std::runtime_error : public std::exception

    std::runtime_error — ещё один базовый тип для нескольких видов исключений. Он говорит о том, что исключение относится скорее не к предусмотренной ошибке, а к выявленной в процессе выполнения.

    При этом, если std::logic_error подразумевает конкретную причину ошибки — нарушение конкретного условия, — то std::runtime_error говорит о том, что что-то идёт не так, но первопричина может быть не вполне очевидна.

    Интерфейс такой же, как и у logic_error: класс принимает описание ошибки в конструкторе и переопределяет метод what() базового класса std::exception.

    class CommandLineParsingError : public std::runtime_error {
    public:
        // этой строкой импортируем конструктор из базового класса:
        using runtime_error::runtime_error;
    };
    
    class ZeroDenominatorError : public std::runtime_error {
    public:
        // используем готовое сообщение:
        ZeroDenominatorError() : std::runtime_error("Знаменатель не может быть нулём") {
        }
    }

    Рассмотрим некоторые важные производные классы:

    • std::regex_error. Исключение, возникшее в процессе работы с регулярными выражениями. Например, при неверном синтаксисе регулярного выражения.
    • std::system_error. Широкий класс исключений, связанных с потоками, вводом-выводом или файловой системой.
    • std::format_error. Исключение, возникшее при работе функции std::format.

    std::bad_alloc : public std::exception

    У std::exception есть и другие наследники. Самый важный — std::bad_alloc. Его может выбрасывать операция new. Это исключение — слабое место многих программ и головная боль многих разработчиков, ведь оно может возникать практически везде — в любом месте, где есть динамическая аллокация. То есть при:

    • вставке в любой контейнер;
    • копировании любого контейнера, например, обычной строки;
    • создании умного указателя unique_ptr или shared_ptr;
    • копировании объекта, содержащего контейнер;
    • прямом вызове new (надеемся, что вы так не делаете);
    • работе с потоками ввода-вывода;
    • работе алгоритмов;
    • вызове корутин;
    • в пользовательских классах и библиотеках — практически при любых операциях.

    При обработке bad_alloc нужно соблюдать осторожность и избегать других динамических аллокаций.

    #include 
    #include 
    #include 
    #include 
    
    int main() {
        std::vector vec;
        try {
            while (true) {
                vec.push_back(std::string(10000000, 'a'));
            }
        }
        catch (const std::bad_alloc& e) {
            std::cout << "Место закончилось после вставки " << vec.size() << " элементов" << std::endl;
        }
    }

    Возможный вывод: «Место закончилось после вставки 2640 элементов».

    При аллокациях возможна также ошибка std::bad_array_new_length, производная от bad_alloc. Она возникает при попытке выделить слишком большое, слишком маленькое (меньше, чем задано элементов для инициализации) либо отрицательное количество памяти.

    Также при аллокации можно запретить new выбрасывать исключение. Для этого пишем (std::nothrow) после new:

    int main()
    {
        int* m = new (std::nothrow) int [0xFFFFFFFFFFFFFFULL];
        std::cout << m; // выведет 0
        delete[] m;
    }

    В случае ошибки операция будет возвращать нулевой указатель.

    bad_alloc настолько сложно учитывать, что многие даже не пытаются это делать. Мотивация такая: если память закончилась, то всё равно программе делать уже нечего. Лучше поскорей вызвать std::terminate и завершиться.

    Заключение

    В этой части мы разобрали, как создавать исключения C++, какие они бывают и как с ними работать. Разобрали ключевые слова try, catch и throw.

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

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

    C++ позволяет выразительно обрабатывать исключения, он аккуратен при удалении всех объектов и освобождении ресурсов. Будьте аккуратны и вы, и тогда всё получится. Каждому исключению — по обработчику.

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

    Добавлено 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 / Стандартная библиотека шаблоновДля начинающихКласс (программирование)НаследованиеОбработка ошибокОбучениеПрограммирование

    Определено в заголовке <stdexcept>
    class runtime_error;

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

    Исключения типа std::runtime_error генерируются следующими компонентами стандартной библиотеки: std::locale::locale и std::locale::combine .

    Кроме того, следующие стандартные типы исключений являются производными от std::runtime_error :

    • std::range_error
    • std::overflow_error
    • std::underflow_error
    • std::chrono::ambiguous_local_time
    • std::chrono::nonexistent_local_time
    • std::format_error
    (since C++20)

    Member functions

    (constructor)

    создает новый объект runtime_error с заданным сообщением
    (функция публичного члена)

    operator=

    заменяет объект runtime_error
    (функция публичного члена)

    what

    возвращает пояснительную строку
    (функция публичного члена)

    std::runtime_error::runtime_error

    runtime_error( const std::string& what_arg );
    (1)
    runtime_error( const char* what_arg );
    (2) (since C++11)
    (3)
    runtime_error( const runtime_error& other );
    (until C++11)
    runtime_error( const runtime_error& other ) noexcept;
    (since C++11)

    1-2) Создает объект исключения с what_arg в качестве пояснительной строки, к которой можно получить доступ через what() .

    3) Конструктор копирования. Если *this и other имеют динамический тип std::runtime_error , то std::strcmp(what(), other.what()) == 0 (начиная с C++11).

    Parameters

    what_arg explanatory string
    other другой объект исключения для копирования

    Exceptions

    Notes

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

    std::runtime_error::operator=

    runtime_error& operator=( const runtime_error& other );
    (until C++11)
    runtime_error& operator=( const runtime_error& other ) noexcept;
    (since C++11)

    Сопоставляет содержимое с содержимым other . Если *this и other имеют динамический тип std::runtime_error , то std::strcmp(what(), other.what()) == 0 после присваивания (начиная с C++11).

    Parameters

    other другой объект исключения для присвоения

    Return value

    *this.

    std::runtime_error::what

    virtual const char* what() const throw();
    (until C++11)
    virtual const char* what() const noexcept;
    (since C++11)

    Возвращает пояснительную строку.

    Parameters

    (none).

    Return value

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

    Notes

    Реализации разрешены, но не обязаны переопределять what() .

    Наследуется от std :: exception

    Member functions

    [virtual]

    уничтожает объект исключения
    (виртуальная публичная функция-член std::exception )

    [virtual]

    возвращает пояснительную строку
    (виртуальная публичная функция-член std::exception )


    C++

    • std::rethrow_exception

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

    • std::rethrow_if_nested

      Если E не является полиморфным типом класса,то std::nested_exception недоступная неоднозначная база этого эффекта.

    • std::set_terminate

      Убедитесь,что новая функция глобального обработчика завершения возвращает ранее установленный std::terminate_handler.

    • std::set_unexpected

      Делает f новый глобальный std::unexpected_handler,возвращающий ранее установленную функцию,потокобезопасным.

    Типы исключений

    Последнее обновление: 05.10.2017

    Кроме типа exception в C++ есть еще несколько производных типов исключений, которые могут использоваться при различных ситуациях. Основные из них:

    • runtime_error: общий тип исключений, которые возникают во время выполнения

    • range_error: исключение, которое возникает, когда полученный результат превосходит допустимый диапазон

    • overflow_error: исключение, которое возникает, если полученный результат превышает допустимый диапазон

    • underflow_error: исключение, которое возникает, если полученный в вычислениях результат имеет недопустимые отрицательное значение (выход за нижнюю допустимую границу значений)

    • logic_error: исключение, которое возникает при наличии логических ошбок к коде программы

    • domain_error: исключение, которое возникает, если для некоторого значения, передаваемого в функцию, не определено результата

    • invalid_argument: исключение, которое возникает при передаче в функцию некорректного аргумента

    • length_error: исключение, которое возникает при попытке создать объект большего размера, чем допустим для данного типа

    • out_of_range: исключение, которое возникает при попытке доступа к элементам вне допустимого диапазона

    Типы исключений в C++ и std::exception

    Большинство этих типов определено в заголовочном файле stdexcept, за исключением класса bad_alloc,
    который определн в файле new, и класса bad_cast, который определен в файле
    type_info.

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

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

    При использовании нескольких блоков catch вначале помещаются блоки catch, которые обрабатывают более частные исключения, а только потом блоки catch с более
    общими типами исключений:

    #include <iostream>
    #include <exception>
    #include <stdexcept>
    
    double divide(int, int);
    
    int main()
    {
    	int x = 500;
    	int y = 0;
    	try
    	{
    		double z = divide(x, y);
    		std::cout << z << std::endl;
    	}
    	catch (std::overflow_error err)
    	{
    		std::cout << "Overflow_error: " << err.what() << std::endl;
    	}
    	catch (std::runtime_error err)
    	{
    		std::cout << "Runtime_error: " << err.what() << std::endl;
    	}
    	catch (std::exception err)
    	{
    		std::cout << "Exception!!!"<< std::endl;
    	}
    	std::cout << "The End..." << std::endl;
    	return 0;
    }
    
    double divide(int a, int b)
    {
    	if (b == 0)
    		throw std::runtime_error("Division by zero!");
    	return a / b;
    }
    

    Здесь функция divide, если параметр b равен 0, выбрасывает исключение типа runtime_error. Исключение инициализируется
    сообщением об ошибке «Division by zero!».

    В функции main конструкция try..catch использует три блока catch. Причем последний блок представляет самый общий тип исключений exception. Второй блок
    обрабатывает исключения типа runtime_error, производный от exception. А первый блок обрабатывает исключения типа overflow_error, который является производным от
    runtime_error.

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

    Runtime_error: Division by zero!
    The End...
    

    Понравилась статья? Поделить с друзьями:
  • Cp3525 error 59 f0
  • Cover edit resolution error закрыть
  • Cover edit resolution error вк обложка
  • Cout was not declared in this scope как исправить
  • Cout is ambiguous c ошибка