Throw std 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
  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++11: auto, lambda, variadic templates. Но как-то обошли стороной новые возможности работы с исключениями, которые предоставляет язык и стандартная библиотека.

От предыдущей версии стандарта остался механизм генерации исключений (throw), проверка того, что мы находимся в процессе обработки исключения (std::uncaught_exception), механизм остановки, если исключение не было обработано. Также есть иерархия стандартных исключений на базе класса std::exception.

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

exception_ptr

Итак, самое первое, с чем мы можем столкнуться — это std::exception_ptr. Этот тип позволяет в себе хранить исключение абсолютно любого типа. Стандарт не оговаривает каким образом получен этот тип. Это может быть typedef, это может быть реализация класса. Его поведение сходно поведению std::shared_ptr, то есть его можно копировать, передавать в качестве параметра, при этом само исключение не копируется. Основное предназначение exception_ptr — это передача исключений в качестве параметров функции, возможна передача исключений между потоками. Таким образом, объекты данного типа позволяют сделать обработку ошибок более гибкой:

struct some_exception {
	explicit some_exception(int x): v(x) {
		std::cout << " int ctor" << std::endl;
	}

	some_exception(const some_exception & e): v(e.v) {
		std::cout << " copy ctor" << std::endl;
	}

	int v;
};

std::exception_ptr throwExceptionAndCaptureExceptionPtr() {
	std::exception_ptr currentException;
	try {
		const int throwValue = 10;
		std::cout << "throwing           " << throwValue << "..." << std::endl;
		throw some_exception(throwValue);

	} catch (...) {
		 currentException = std::current_exception();
	}

	return currentException;
}

void rethrowException(std::exception_ptr ePtr) {
	try {
		if (ePtr) {
			std::rethrow_exception(ePtr);
		}

	} catch (const some_exception & e) {
		std::cout << "catched int value: " << e.v << std::endl;
	}

	std::exception_ptr anotherExceptionPtr = ePtr;
	try {
		if (anotherExceptionPtr) {
			std::rethrow_exception(anotherExceptionPtr);
		}

	} catch (const some_exception & e) {
		std::cout << "catched int value: " << e.v << std::endl;
	}
}

void checkMakeExceptionPtr() {
	std::exception_ptr currentException = std::make_exception_ptr(some_exception(20));
	std::cout << "exception_ptr constructed" << std::endl;

	rethrowException(currentException);
}

void exceptionPtrSample() {
	rethrowException(throwExceptionAndCaptureExceptionPtr());
	checkMakeExceptionPtr();
}

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

throwing 10…
int ctor
catched int value: 10
catched int value: 10
int ctor
copy ctor
copy ctor
exception_ptr constructed
catched int value: 20
catched int value: 20

Для того чтобы можно было удобно работать с exception_ptr, есть несколько вспомогательных функций:

  • current_exception — данная функция возвращает exception_ptr. Если мы находимся внутри блока catch, то возвращает exception_ptr, который содержит обрабатываемое в данный момент текущим потоком исключение, если вызывать ее вне блока catch, то она вернет пустой объект exception_ptr
  • rethrow_exception — данная функция бросает исключение, которое содержится в exception_ptr. Если входной параметр не содержит исключения (пустой объект), то результат не определен. При этом msvc кидает std::bad_exception, а программа собранная с помощью gcc-4.7.2 неожиданно завершается.
  • make_exception_ptr — данная функция, может сконструировать exception_ptr без бросания исключения. Ее предназначение аналогично функции std::make_shared — конструирование объекта. Ее выполнение аналогично функции throwExceptionAndCaptureExceptionPtr. В реализации от gcc-4.7.2 make_exception_ptr делает два копирования объекта some_exception.
Передача исключений между потоками

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

void worker(std::promise<void> & p) {
	try {
		throw std::runtime_error("exception from thread");
	} catch (...) {
		p.set_exception(std::current_exception());
	}
}

void checkThreadAndException() {
	std::promise<void> p;
	auto result = p.get_future();

	std::thread t(worker, ref(p));
	t.detach();

	try {
		result.get();
	} catch (const std::runtime_error & e) {
		std::cout << "runtime error catched from async worker" << std::endl;
	}
}

Вообще, многопоточность в C++11 — это обширная тема, там есть свои тонкости, нюансы и о них следует писать отдельно. Сейчас мы рассмотрим этот пример только ради передачи исключения. Запускаем функцию worker в отдельном потоке, и эта функция кидает исключение. Объект класса promise позволяет организовать связь между разными потоками, и атомарно передать значения из одного потока в другой (или исключение). В данном примере мы как раз пользуемся методом set_exception, который принимает exception_ptr в качестве параметра. Для того чтобы получить значение, мы создаем объект класса future — это наш result и вызываем метод get. Также необходимо у потока вызвать метод detach или join, так как при разрушении объекта t в деструкторе проверяется, чтобы joinable() == false, иначе вызывается std::terminate. Скорее всего, это связано с тем, чтобы программист не «отпускал потоки на волю», а всегда следил за ними (либо отпускал явно с помощью метода detach)

Отдельно стоит сказать об использовании многопоточности в gcc-4.7. Изначально этот пример у меня не работал (выбрасывал исключение), погуглив, я выяснил, что для использования std::thread необходимо передать линкеру флаг -pthread. Так как я использую CMake в качестве системы сборки, то эта задача упрощается (тут сложность может возникнуть при использовании gcc на разных платформах, например на sparc solaris используется флаг -thread вместо -pthread) — есть специальный CMake модуль Threads, в котором эта проблема решена:

find_package(Threads REQUIRED)
#…
target_link_libraries(cxx_exceptions ${CMAKE_THREAD_LIBS_INIT})

Nested exceptions

Как видно из названия, данный механизм позволяет «прицепить» к кидаемому исключению другие исключения (которые могли быть брошены раньше). Например, если у нас есть своя иерархия исключений, то мы можем ловить все «сторонние» исключения, прицеплять их к своим исключениям, а при обработке своих исключений мы можем выводить доп. информацию, которая к ним «прицеплена» (например, при отладке, мы можем печатать информацию о сторонних исключениях). Хороший пример использования nested exception приведен на cppreference.com, мой пример, отчасти пересекается с ним:

struct third_party_exception {
	explicit third_party_exception(int e) : code(e) {
	}

	int code;
};


void third_party_worker() {
	throw third_party_exception(100);
}

class our_exception : public std::runtime_error {
public:
	our_exception(const std::string & e) : std::runtime_error("our error: " + e) {
	}
};

void ourWorker() {
	try {
		third_party_worker();
	} catch (...) {
		throw_with_nested(our_exception("worker failed"));
	}
}

void exceptionHandler(const our_exception & e, bool catchNested) {
	std::cerr << "our exception catched: " << e.what();

	if (catchNested) {
		try {
			rethrow_if_nested(e);
		} catch (const third_party_exception & e) {
			std::cerr << ", low level reason is: " << e.code;
		}
	}

	std::cerr << std::endl;
}

void exceptionProcessing() {
	try {
		ourWorker();
	} catch (const our_exception & e) {
		exceptionHandler(e, false);
	}

	try {
		ourWorker();
	} catch (const our_exception & e) {
		exceptionHandler(e, true);
	}
}

Итак, у нас есть сторонняя функция, которая кидает исключение, мы можем написать адаптер, который ловит «сторонние» исключения, и из них делает «наше» исключение: в качестве «сторонней» функции и «нашей» функции выступают соответственно third_party_worker и ourWorker. Мы ловим все исключения, и бросаем далее уже наше (our_exception) исключение, при этом, к нему было прицеплено какое-то (мы в принципе можем и не знать какое) «стороннее» исключение. После этого работаем уже с нашими исключениями. При этом, если нам понадобится более детальная информация о том, что происходило на «нижнем» уровне, то мы всегда можем вызвать функцию rethrow_if_nested. Данная функция анализирует, если ли прицепленное (вложенное) исключение, и если есть, то бросает это вложенное исключение. Функция exceptionHandler принимает «наше» исключение и дополнительный флаг, который разрешает или запрещает вывод информации о стороннем исключении. Мы можем управлять выводом сторонних исключений, управляя параметром catchNested, например, из файла конфигурации (или в зависимости от сборки — Release, Debug).

Для работы с nested exception есть один класс и две функции:

  • nested_exception — данный класс «подмешивается» к бросаемому объекту при вызове функции std::throw_with_nested — этот класс также позволяет вернуть вложенное исключение (exception_ptr) с помощью метода nested_ptr. Также этот класс имеет метод rethrow_nested, который бросает вложенное исключение
  • throw_with_nested — данная шаблонная функция принимает объект некоторого типа (назовем его InputType), и бросает объект который является наследником std::nested_exception и нашего InputType (в реализации от gcc — это шаблонный тип, который наследуется от nested_exception и InputType). Таким образом, мы можем ловить как объект нашего типа, так и объект типа nested_exception и уже потом получать наш тип через метод nested_ptr
  • rethrow_if_nested — данная функция определяет, есть ли у объекта вложенное исключение, и если есть, то бросает его. Реализация может использовать dynamic_cast для определения наследования

В принципе, механизм nested exception довольно интересный, хотя реализация может быть довольно тривиальна, ее можно сделать самому, с помощью ранее описанных функций current_exception и rethrow_exception. В той же реализации gcc, класс nested_exception содержит одно поле типа exception_ptr, которое инициализируется в конструкторе с помощью функции current_exception, а реализация метода rethrow_nested просто вызывает функцию rethrow_exception.

Спецификация noexcept

Данный механизм можно рассматривать как расширенный (и ныне устаревший) механизм throw(). Основное его предназначение как и раньше — гарантия того что, функция не бросит исключение (если гарантия нарушается, то вызывается std::terminate).

Используется этот механизм в двух видах, аналогично старому throw()

void func() noexcept {
//...
}

И в новом виде:

void func() noexcept(boolean_expression_known_at_compile_time) {
//...
}

При этом, если значение выражения вычислено как истина, то функция помечается как noexcept, иначе, такой гарантии нет.
Также есть соответствующий оператор noexcept(expression), который тоже выполняется в compile time, этот оператор возвращает истину, если выражение не бросает исключение:

void noexceptSample() {
	cout << "noexcept int():         " << noexcept(int()) << endl;
	cout << "noexcept vector<int>(): " << noexcept(vector<int>()) << endl;
}

Данный код для gcc-4.7.2 выводит:

noexcept int(): 1
noexcept vector<int>(): 0

Здесь мы видим, что конструктор встроенного типа int не бросает исключение, а конструктор вектора может бросить (не помечен как noexcept).
Это удобно применять в шаблонном метапрограмировании, используя данный оператор мы можем написать шаблонную функцию, которая в зависимости от параметра шаблона может быть помечена как noexcept или нет:

template <typename InputTypeT>
void func() noexcept(noexcept(InputTypeT())) {
	InputTypeT var;
	/// do smth with var
	std::cout << "func called, object size: " << sizeof(var) << std::endl;
}

void noexceptSample() {
	std::cout << "noexcept int():         " << noexcept(int()) << std::endl;
	std::cout << "noexcept vector<int>(): " << noexcept(std::vector<int>()) << std::endl << std::endl;

	/// @note function is not actually called
	std::cout << "noexcept func<int>:         " << noexcept(func<int>()) << std::endl;
	std::cout << "noexcept func<vector<int>>: " << noexcept(func<std::vector<int>>()) << std::endl;
}

Данный пример выводит:

noexcept int(): 1
noexcept vector<int>(): 0

noexcept func<int>: 1
noexcept func<vector<int>>: 0

Резюме

Стандарт C++11 привнес много нового в обработку ошибок, конечно, ключевой особенностью здесь остается exception_ptr и возможность передачи произвольных исключений как обычные объекты (в функции, передавать исключения между потоками). Раньше, в каждом потоке приходилось писать развесистый try… catch для всех исключений, а этот функционал существенно минимизирует количество try… catch кода.

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

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

Как обычно, все примеры выложены на github

Update 1. Убрал из примеров using namespace std, теперь видно какие сущности относятся к стандартной библиотеке, а какие нет.

Поговорим об исключениях в 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++».

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

Последнее обновление: 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...

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

Исключение исключений

Блок try/catch используется для обнаружения исключений. Код в разделе try — это код, который может вызывать исключение, а код в выражении (-ах) catch обрабатывает исключение.

#include <iostream>
#include <string>
#include <stdexcept>

int main() {
  std::string str("foo");
  
  try {
      str.at(10); // access element, may throw std::out_of_range
  } catch (const std::out_of_range& e) {
      // what() is inherited from std::exception and contains an explanatory message
      std::cout << e.what();
  }
}

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

std::string str("foo");
  
try {
    str.reserve(2); // reserve extra capacity, may throw std::length_error
    str.at(10); // access element, may throw std::out_of_range
} catch (const std::length_error& e) {
    std::cout << e.what();
} catch (const std::out_of_range& e) {
    std::cout << e.what();
}

Классы исключений, которые выведены из общего базового класса, могут быть пойманы с помощью одного предложения catch для общего базового класса. Вышеприведенный пример может заменить два предложения catch для std::length_error и std::out_of_range с одним предложением для std:exception :

std::string str("foo");
  
try {
    str.reserve(2); // reserve extra capacity, may throw std::length_error
    str.at(10); // access element, may throw std::out_of_range
} catch (const std::exception& e) {
    std::cout << e.what();
}

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

try {
    /* Code throwing exceptions omitted. */
} catch (const std::exception& e) {
    /* Handle all exceptions of type std::exception. */
} catch (const std::runtime_error& e) {
    /* This block of code will never execute, because std::runtime_error inherits
       from std::exception, and all exceptions of type std::exception were already
       caught by the previous catch clause. */
}

Другая возможность — обработчик catch-all, который поймает любой брошенный объект:

try {
    throw 10;
} catch (...) {
    std::cout << "caught an exception";
}

Исключение Rethrow (распространение)

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

try {
    ... // some code here
} catch (const SomeException& e) {
    std::cout << "caught an exception";
    throw;
}

Использование throw; без аргументов будет повторно выбрано текущее исключение.

C ++ 11

Чтобы восстановить управляемую std::exception_ptr , стандартная библиотека C ++ имеет функцию rethrow_exception которая может быть использована путем включения заголовка <exception> в вашу программу.

#include <iostream>
#include <string>
#include <exception>
#include <stdexcept>
 
void handle_eptr(std::exception_ptr eptr) // passing by value is ok
{
    try {
        if (eptr) {
            std::rethrow_exception(eptr);
        }
    } catch(const std::exception& e) {
        std::cout << "Caught exception "" << e.what() << ""n";
    }
}
 
int main()
{
    std::exception_ptr eptr;
    try {
        std::string().at(1); // this generates an std::out_of_range
    } catch(...) {
        eptr = std::current_exception(); // capture
    }
    handle_eptr(eptr);
} // destructor for std::out_of_range called here, when the eptr is destructed

Функция Try Blocks В конструкторе

Единственный способ исключить исключение в списке инициализаторов:

struct A : public B
{
    A() try : B(), foo(1), bar(2)
    {
        // constructor body 
    }
    catch (...)
    {
        // exceptions from the initializer list and constructor are caught here
        // if no exception is thrown here
        // then the caught exception is re-thrown.
    }
 
private:
    Foo foo;
    Bar bar;
};

Функция Try Block для регулярной функции

void function_with_try_block() 
try
{
    // try block body
} 
catch (...) 
{ 
    // catch block body
}

Это эквивалентно

void function_with_try_block() 
{
    try
    {
        // try block body
    } 
    catch (...) 
    { 
        // catch block body
    }
}

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

Функция main разрешено иметь функцию попробовать блок , как и любой другой функции, но main функция попытка блока «s не перехватывать исключения , которые происходят во время строительства нелокального статической переменной или уничтожения любой статической переменной. Вместо этого вызывается std::terminate .

Функция Try Blocks В деструкторе

struct A
{
    ~A() noexcept(false) try
    {
        // destructor body 
    }
    catch (...)
    {
        // exceptions of destructor body are caught here
        // if no exception is thrown here
        // then the caught exception is re-thrown.
    }
};

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

Лучшая практика: бросать по значению, вызывать по ссылке const

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

try {
    // throw new std::runtime_error("Error!");   // Don't do this!
    // This creates an exception object
    // on the heap and would require you to catch the
    // pointer and manage the memory yourself. This can
    // cause memory leaks!
    
    throw std::runtime_error("Error!");
} catch (const std::runtime_error& e) {
    std::cout << e.what() << std::endl;
}

Одной из причин, почему ловить по ссылке является хорошей практикой, является то, что она устраняет необходимость в восстановлении объекта при передаче в блок catch (или при распространении на другие блоки catch). Захват по ссылке также позволяет обрабатывать исключения из-за полиморфизма и избегать фрагментации объектов. Однако, если вы воссоздаете исключение (например, throw e; см. Пример ниже), вы все равно можете получить фрагмент объекта, потому что throw e; оператор делает копию исключения как любой тип объявлен:

#include <iostream>

struct BaseException {
    virtual const char* what() const { return "BaseException"; }
};

struct DerivedException : BaseException {
    // "virtual" keyword is optional here
    virtual const char* what() const { return "DerivedException"; }
};

int main(int argc, char** argv) {
    try {
        try {
            throw DerivedException();
        } catch (const BaseException& e) {
            std::cout << "First catch block: " << e.what() << std::endl;
            // Output ==> First catch block: DerivedException

            throw e; // This changes the exception to BaseException
                     // instead of the original DerivedException!
        }
    } catch (const BaseException& e) {
        std::cout << "Second catch block: " << e.what() << std::endl;
        // Output ==> Second catch block: BaseException
    }
    return 0;
}

Если вы уверены, что не собираетесь делать что-либо, чтобы изменить исключение (например, добавить информацию или изменить сообщение), catching by const reference позволяет компилятору сделать оптимизацию и повысить производительность. Но это все равно может привести к сращиванию объектов (как видно из приведенного выше примера).

Предупреждение. Не забывайте бросать непреднамеренные исключения в блоки catch , особенно связанные с распределением дополнительной памяти или ресурсов. Например, при построении logic_error , runtime_error или их подклассов может возникнуть bad_alloc из-за bad_alloc памяти при копировании строки исключения, потоки ввода-вывода могут возникать при регистрации с соответствующими масками маски исключений и т. Д.

Вложенное исключение

C ++ 11

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

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

std::nested_exception позволяет std::nested_exception исключения из-за std::throw_with_nested :

#include <stdexcept>
#include <exception>
#include <string>
#include <fstream>
#include <iostream>

struct MyException
{
    MyException(const std::string& message) : message(message) {}
    std::string message;
};

void print_current_exception(int level)
{
    try {
        throw;
    } catch (const std::exception& e) {
        std::cerr << std::string(level, ' ') << "exception: " << e.what() << 'n';
    } catch (const MyException& e) {
        std::cerr << std::string(level, ' ') << "MyException: " << e.message << 'n';
    } catch (...) {
        std::cerr << "Unkown exceptionn";
    }
}

void print_current_exception_with_nested(int level =  0)
{
    try {
        throw;
    } catch (...) {
        print_current_exception(level);
    }    
    try {
        throw;
    } catch (const std::nested_exception& nested) {
        try {
            nested.rethrow_nested();
        } catch (...) {
            print_current_exception_with_nested(level + 1); // recursion
        }
    } catch (...) {
        //Empty // End recursion
    }
}

// sample function that catches an exception and wraps it in a nested exception
void open_file(const std::string& s)
{
    try {
        std::ifstream file(s);
        file.exceptions(std::ios_base::failbit);
    } catch(...) {
        std::throw_with_nested(MyException{"Couldn't open " + s});
    }
}
 
// sample function that catches an exception and wraps it in a nested exception
void run()
{
    try {
        open_file("nonexistent.file");
    } catch(...) {
        std::throw_with_nested( std::runtime_error("run() failed") );
    }
}
 
// runs the sample function above and prints the caught exception
int main()
{
    try {
        run();
    } catch(...) {
        print_current_exception_with_nested();
    }
}

Возможный выход:

exception: run() failed
 MyException: Couldn't open nonexistent.file
  exception: basic_ios::clear

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

СТД :: uncaught_exceptions

C ++ 17

C ++ 17 вводит int std::uncaught_exceptions() (чтобы заменить ограниченный bool std::uncaught_exception() ), чтобы узнать, сколько исключений в настоящее время не реализовано. Это позволяет классу определить, уничтожен ли он во время разворачивания стека или нет.

#include <exception>
#include <string>
#include <iostream>

// Apply change on destruction:
// Rollback in case of exception (failure)
// Else Commit (success)
class Transaction
{
public:
    Transaction(const std::string& s) : message(s) {}
    Transaction(const Transaction&) = delete;
    Transaction& operator =(const Transaction&) = delete;
    void Commit() { std::cout << message << ": Commitn"; }
    void RollBack() noexcept(true) { std::cout << message << ": Rollbackn"; }

    // ...

    ~Transaction() {
        if (uncaughtExceptionCount == std::uncaught_exceptions()) {
            Commit(); // May throw.
        } else { // current stack unwinding
            RollBack();
        }
    }

private:
    std::string message;
    int uncaughtExceptionCount = std::uncaught_exceptions();
};

class Foo
{
public:
    ~Foo() {
        try {
            Transaction transaction("In ~Foo"); // Commit,
                                            // even if there is an uncaught exception
            //...
        } catch (const std::exception& e) {
            std::cerr << "exception/~Foo:" << e.what() << std::endl;
        }
    }
};

int main()
{
    try {
        Transaction transaction("In main"); // RollBack
        Foo foo; // ~Foo commit its transaction.
        //...
        throw std::runtime_error("Error");
    } catch (const std::exception& e) {
        std::cerr << "exception/main:" << e.what() << std::endl;
    }
}

Выход:

In ~Foo: Commit
In main: Rollback
exception/main:Error

Пользовательское исключение

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

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

#include <exception>

class Except: virtual public std::exception {
    
protected:

    int error_number;               ///< Error number
    int error_offset;               ///< Error offset
    std::string error_message;      ///< Error message
    
public:

    /** Constructor (C++ STL string, int, int).
     *  @param msg The error message
     *  @param err_num Error number
     *  @param err_off Error offset
     */
    explicit 
    Except(const std::string& msg, int err_num, int err_off):
        error_number(err_num),
        error_offset(err_off),
        error_message(msg)
        {}

    /** Destructor.
     *  Virtual to allow for subclassing.
     */
    virtual ~Except() throw () {}

    /** Returns a pointer to the (constant) error description.
     *  @return A pointer to a const char*. The underlying memory
     *  is in possession of the Except object. Callers must
     *  not attempt to free the memory.
     */
    virtual const char* what() const throw () {
       return error_message.c_str();
    }
    
    /** Returns error number.
     *  @return #error_number
     */
    virtual int getErrorNumber() const throw() {
        return error_number;
    }
    
    /**Returns error offset.
     * @return #error_offset
     */
    virtual int getErrorOffset() const throw() {
        return error_offset;
    }

};

Пример броска catch:

try {
    throw(Except("Couldn't do what you were expecting", -12, -34));
} catch (const Except& e) {
    std::cout<<e.what()
             <<"nError number: "<<e.getErrorNumber()
             <<"nError offset: "<<e.getErrorOffset();
}

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

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

Вы также можете унаследовать этот класс:

#include <stdexcept>

class Except: virtual public std::runtime_error {
    
protected:

    int error_number;               ///< Error number
    int error_offset;               ///< Error offset
    
public:

    /** Constructor (C++ STL string, int, int).
     *  @param msg The error message
     *  @param err_num Error number
     *  @param err_off Error offset
     */
    explicit 
    Except(const std::string& msg, int err_num, int err_off):
        std::runtime_error(msg)
        {
            error_number = err_num;
            error_offset = err_off;
            
        }

    /** Destructor.
     *  Virtual to allow for subclassing.
     */
    virtual ~Except() throw () {}
    
    /** Returns error number.
     *  @return #error_number
     */
    virtual int getErrorNumber() const throw() {
        return error_number;
    }
    
    /**Returns error offset.
     * @return #error_offset
     */
    virtual int getErrorOffset() const throw() {
        return error_offset;
    }

};

Обратите внимание, что я не переопределил функцию what() из базового класса ( std::runtime_error ), то есть мы будем использовать версию класса base what() . Вы можете переопределить его, если у вас есть дальнейшая повестка дня.

std::exception is the class whose only purpose is to serve as the base class in the exception hierarchy. It has no other uses. In other words, conceptually it is an abstract class (even though it is not defined as abstract class in C++ meaning of the term).

std::runtime_error is a more specialized class, descending from std::exception, intended to be thrown in case of various runtime errors. It has a dual purpose. It can be thrown by itself, or it can serve as a base class to various even more specialized types of runtime error exceptions, such as std::range_error, std::overflow_error etc. You can define your own exception classes descending from std::runtime_error, as well as you can define your own exception classes descending from std::exception.

Just like std::runtime_error, standard library contains std::logic_error, also descending from std::exception.

The point of having this hierarchy is to give user the opportunity to use the full power of C++ exception handling mechanism. Since ‘catch’ clause can catch polymorphic exceptions, the user can write ‘catch’ clauses that can catch exception types from a specific subtree of the exception hierarchy. For example, catch (std::runtime_error& e) will catch all exceptions from std::runtime_error subtree, letting all others to pass through (and fly further up the call stack).

P.S. Designing a useful exception class hierarchy (that would let you catch only the exception types you are interested in at each point of your code) is a non-trivial task. What you see in standard C++ library is one possible approach, offered to you by the authors of the language. As you see, they decided to split all exception types into «runtime errors» and «logic errors» and let you proceed from there with your own exception types. There are, of course, alternative ways to structure that hierarchy, which might be more appropriate in your design.

Update: Portability Linux vs Windows

As Loki Astari and unixman83 noted in their answer and comments below, the constructor of the exception class does not take any arguments according to C++ standard. Microsoft C++ has a constructor taking arguments in the exception class, but this is not standard. The runtime_error class has a constructor taking arguments (char*) on both platforms, Windows and Linux. To be portable, better use runtime_error.

(And remember, just because a specification of your project says your code does not have to run on Linux, it does not mean it does never have to run on Linux.)

Signals an erroneous condition and executes an error handler.

Syntax

throwexpression (1)
throw (2)

Explanation

See try-catch block for more information abouttryandcatch(exception handler) blocks 1) First, copy-initializes theexception objectfrom expression

  • This may call the move constructor for rvalue expression. Even if copy initialization selects the move constructor, copy initialization from lvalue must be well-formed, and the destructor must be accessible
(since C++11)
  • This may also call the move constructor for expressions that name local variables or function or catch-clause parameters whose scope does not extend past the innermost enclosing try-block (if any), by same overload resolution as in return statement
(since C++17)
  • The copy/move(since C++11) may be subject to copy elision

then transfers control to the exception handler with the matching type for which the compound statement or member initializer list that follows the keyword try was most recently entered and not exited by this thread of execution.

2) Rethrows the currently handled exception. Abandons the execution of the current catch block and passes control to the next matching exception handler (but not to another catch clause after the same try block: its compound-statement is considered to have been ‘exited’), reusing the existing exception object: no new objects are made. This form is only allowed when an exception is presently being handled (it calls std::terminate if used otherwise). The catch clause associated with a function-try-block must exit via rethrowing if used on a constructor.

See std::terminate and std::unexpected(until C++17) for the handling of errors that arise during exception handling.

The exception object

The exception object is a temporary object in unspecified storage that is constructed by the throw expression.

The type of theexception object is the static type of expression with top-level cv-qualifiers removed. Array and function types are adjusted to pointer and pointer to function types, respectively. If the type of the exception object would be an incomplete type, an abstract class type, or pointer to incomplete type other than pointer to (cv-qualified) void, the throw-expression is a compile-time error. If the type of expression is a class type, its copy/move(since C++11) constructor and destructor must be accessible even if copy elision takes place.

Unlike other temporary objects, the exception object is considered to be an lvalue argument when initializing the catch clause parameters, so it can be caught by lvalue reference, modified, and rethrown.

The exception object persists until the last catch clause exits other than by rethrowing (if not by rethrowing, it is destroyed immediately after the destruction of the catch clause’s parameter), or until the last std::exception_ptr that references this object is destroyed (in which case the exception object is destroyed just before the destructor of std::exception_ptr returns.

Stack unwinding

Once the exception object is constructed, the control flow works backwards (up the call stack) until it reaches the start of a try block, at which point the parameters of all associated catch blocks are compared, in order of appearance, with the type of the exception object to find a match (see try-catch for details on this process). If no match is found, the control flow continues to unwind the stack until the next try block, and so on. If a match is found, the control flow jumps to the matching catch block.

As the control flow moves up the call stack, destructors are invoked for all objects with automatic storage duration that are constructed, but not yet destroyed, since the corresponding try-block was entered, in reverse order of completion of their constructors. If an exception is thrown from a destructor of a local variable or of a temporary used in a return statement, the destructor for the object returned from the function is also invoked.

If an exception is thrown from a constructor or (rare) from a destructor of an object (regardless of the object’s storage duration), destructors are called for all fully-constructed non-static non-variant members and base classes, in reverse order of completion of their constructors. Variant members of union-like classes are only destroyed in the case of unwinding from constructor, and if the active member changed between initialization and destruction, the behavior is undefined.

If a delegating constructor exits with an exception after the non-delegating constructor successfully completed, the destructor for this object is called.

(since C++11)

If the exception is thrown from a constructor that is invoked by a new-expression, the matching deallocation function is called, if available.

This process is calledstack unwinding.

If any function that is called directly by the stack unwinding mechanism, after initialization of the exception object and before the start of the exception handler, exits with an exception, std::terminate is called. Such functions include destructors of objects with automatic storage duration whose scopes are exited, and the copy constructor of the exception object that is called (if not elided) to initialize catch-by-value arguments.

If an exception is thrown and not caught, including exceptions that escape the initial function of std::thread, the main function, and the constructor or destructor of any static or thread-local objects, then std::terminate is called. It is implementation-defined whether any stack unwinding takes place for uncaught exceptions.

Notes

When rethrowing exceptions, the second form must be used to avoid object slicing in the (typical) case where exception objects use inheritance:

try
{
    std::string("abc").substr(10); // throws std::length_error
}
catch (const std::exception& e)
{
    std::cout << e.what() << 'n';
//  throw e; // copy-initializes a new exception object of type std::exception
    throw;   // rethrows the exception object of type std::length_error
}

The throw-expression is classified as prvalue expression of type void. Like any other expression, it may be a sub-expression in another expression, most commonly in the conditional operator:

double f(double d)
{
    return d > 1e7 ? throw std::overflow_error("too big") : d;
}
 
int main()  
{
    try
    {
        std::cout << f(1e10) << 'n';
    }
    catch (const std::overflow_error& e)
    {
        std::cout << e.what() << 'n';
    }
}

Keywords

throw.

Example

#include <iostream>
#include <stdexcept>
 
struct A
{
    int n;
 
    A(int n = 0): n(n) { std::cout << "A(" << n << ") constructed successfullyn"; }
    ~A() { std::cout << "A(" << n << ") destroyedn"; }
};
 
int foo()
{
    throw std::runtime_error("error");
}
 
struct B
{
    A a1, a2, a3;
 
    B() try : a1(1), a2(foo()), a3(3)
    {
        std::cout << "B constructed successfullyn";
    }
    catch(...)
    {
            std::cout << "B::B() exiting with exceptionn";
    }
 
    ~B() { std::cout << "B destroyedn"; }
};
 
struct C : A, B
{
    C() try
    {
        std::cout << "C::C() completed successfullyn";
    }
    catch(...)
    {
        std::cout << "C::C() exiting with exceptionn";
    }
 
    ~C() { std::cout << "C destroyedn"; }
};
 
int main () try
{
    
    
    
    
    
    C c;
}
catch (const std::exception& e)
{
    std::cout << "main() failed to create C with: " << e.what();
}

Output:

A(0) constructed successfully
A(1) constructed successfully
A(1) destroyed
B::B() exiting with exception
A(0) destroyed
C::C() exiting with exception
main() failed to create C with: error

Defect reports

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

DR Applied to Behavior as published Correct behavior
CWG 499 C++98 an array with unknown bound could not be thrown becuase
its type is incomplete, but an exception object can be
created from the decayed pointer without any problem
apply the type completion
requirement to the
exception object instead
CWG 668 C++98 std::terminate was not called if an exception is thrown
from the destructor of a local non-automatic object
call std::terminate
in this case
CWG 1863 C++11 copy constructor was not required for move-only
exception objects when thrown, but copying allowed later
copy constructor required
CWG 1866 C++98 variant members were leaked on stack unwinding from constructor variant members destroyed
CWG 2176 C++98 throw from a local variable destructor
could skip return value destructor
function return value
added to unwinding

See also

  • copy elision
  • try-catch block
  • noexcept specifier
  • dynamic exception specifications


C++

  • Templates

    A template is C++ entity that defines one of the following: Templates are parameterized by one or more parameters, of three kinds: type non-type and When

  • The this pointer

    The expression this an rvalue (until C++11)a prvalue (since whose address of implicit object parameter which non-static member function being called).

  • Transactional memory

    Transactional memory is concurrency synchronization mechanism that combines groups of statements transactions, are.

  • Phases of translation

    The C++ source file is processed by compiler as if following phases take place, this exact order: Newlines are kept, and it’s unspecified whether non-newline

Понравилась статья? Поделить с друзьями:
  • Throw new error sasl scram server first message client password must be a string
  • Throw new error php
  • Throw new error no sequelize instance passed
  • Throw new error msg
  • Throw new error library dir does not exist libdir