Статьи

С ++ Кратко: приобретение ресурсов — это инициализация

RAII означает «получение ресурсов — это инициализация». RAII — это шаблон проектирования, использующий код C ++ для устранения утечек ресурсов. Утечка ресурсов происходит, когда ресурс, полученный вашей программой, впоследствии не освобождается. Самый знакомый пример — утечка памяти. Поскольку в C ++ нет GC, как в C #, вам нужно быть осторожным, чтобы освободить динамически выделенную память. В противном случае вы потеряете эту память. Утечка ресурсов также может привести к невозможности открытия файла, поскольку файловая система считает, что он уже открыт, невозможности получить блокировку в многопоточной программе или невозможности освободить объект COM.


RAII работает из-за трех основных фактов.

  1. Когда объект длительности автоматического хранения выходит из области видимости, запускается его деструктор.
  2. Когда возникает исключение, все объекты автоматической продолжительности, которые были полностью сконструированы с момента начала последнего блока try, уничтожаются в обратном порядке, в котором они были созданы до вызова любого обработчика catch.
  3. Если вы вкладываете try-блоки, и ни один из обработчиков catch внутреннего try-блока не обрабатывает этот тип исключения, тогда исключение распространяется на внешний try-блок. Все объекты автоматической длительности, которые были полностью построены в этом внешнем блоке try, затем уничтожаются в обратном порядке создания, прежде чем вызывается какой-либо обработчик catch и т. Д., Пока что-то не перехватит исключение или ваша программа не вылетит.

RAII помогает вам высвобождать ресурсы без исключений, просто используя объекты автоматического хранения, которые содержат ресурсы. Это похоже на комбинацию интерфейса System.IDisposable вместе с оператором using в C #. Как только выполнение покидает текущий блок, будь то в результате успешного выполнения или исключения, ресурсы освобождаются.

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

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

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

RAII, конечно, не только о shared_ptr и unique_ptr. Это также относится к другим типам ресурсов, таким как файловый объект, где получение является открытием файла, а деструктор обеспечивает правильное закрытие файла. Это особенно хороший пример, поскольку вам нужно только один раз создать правильный код — когда вы пишете класс, — а не снова и снова, что вам нужно делать, если вы пишете логику закрытия в каждом месте, где вам нужно открыть файл.


Использование RAII описывается его именем: получение динамического ресурса должно завершить инициализацию объекта. Если вы следуете этому шаблону «один ресурс на объект», то невозможно получить утечку ресурса. Либо вы успешно приобретете ресурс, и в этом случае объект, который его инкапсулирует, завершит строительство и будет подвергнут уничтожению, либо попытка получения не удастся, и в этом случае вы не получили ресурс; таким образом, нет ресурса для освобождения.

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

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

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

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

Существует несколько важных типов интеллектуальных указателей. Давайте посмотрим на них.

Уникальный указатель std::unique_ptr предназначен для хранения указателя на динамически размещенный объект. Вы должны использовать этот тип только тогда, когда вы хотите, чтобы существовал один указатель на объект. Это шаблонный класс, который принимает обязательный и необязательный аргумент шаблона. Обязательный аргумент — это тип указателя, который он будет содержать. Например, auto result = std::unique_ptr<int>(new int()); создаст уникальный указатель, который содержит int *. Необязательный аргумент — это тип удалителя. Мы видим, как написать удалитель в следующем примере. Как правило, вы можете избежать указания удалителя, так как default_deleter, который предоставляется вам, если не указан ни один удалитель, охватывает почти все случаи, которые вы можете себе представить.

Класс, в котором переменная-член std::unique_ptr не может иметь конструктор копирования по умолчанию. Семантика копирования отключена для std::unique_ptr . Если вам нужен конструктор копирования в классе, который имеет уникальный указатель, вы должны написать его. Вы также должны написать перегрузку для оператора копирования. Обычно в этом случае вы хотите использовать std::shared_ptr .

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

std::unique_ptr определяется в заголовочном файле <memory>.

std::unique_ptr имеет четыре представляющие интерес функции-члена.

Функция-член get возвращает сохраненный указатель. Если вам нужно вызвать функцию, для которой нужно передать содержащийся указатель, используйте get для получения копии указателя.

Функция-член release также возвращает сохраненный указатель, но release отменяет действие unique_ptr в процессе, заменяя сохраненный указатель нулевым указателем. Если у вас есть функция, в которой вы хотите создать динамический объект и затем вернуть его, сохраняя при этом безопасность исключений, используйте std:unique_ptr для хранения динамически созданного объекта, а затем верните результат вызова release. Это дает вам исключительную безопасность, позволяя вам возвращать динамический объект, не разрушая его с помощью деструктора std::unique_ptr , когда элемент управления выходит из функции после возврата освобожденного значения указателя в конце.

Функция-член swap позволяет двум уникальным указателям обмениваться своими сохраненными указателями, поэтому если A содержит указатель на X, а B содержит указатель на Y, результат вызова A::swap(B); является то, что теперь A будет содержать указатель на Y, а B будет содержать указатель на X. Удалители для каждого также будут поменяны местами, поэтому, если у вас есть пользовательский удалитель для одного или обоих уникальных указателей, будьте уверены, что каждый будет сохранить связанный с ним удалитель.

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

Общий указатель, std::shared_ptr , предназначен для хранения указателя на динамически размещенный объект и для подсчета ссылок на него. Это не волшебство; если вы создадите два общих указателя и передадите им каждый указатель на один и тот же объект, вы получите два общих указателя — каждый со счетчиком ссылок 1, а не 2. Первый уничтоженный освободит базовый ресурс, давая катастрофические результаты, когда вы пытаетесь использовать другой или когда другой уничтожается и пытается освободить уже освобожденный базовый ресурс.

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

Образец: SharedPtrSample \ SharedPtrSample.cpp

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <memory>
#include <iostream>
#include <ostream>
#include «../pchar.h»
 
using namespace std;
 
struct TwoInts
{
    TwoInts(void) : A(), B() { }
    TwoInts(int a, int b) : A(a), B(b) { }
    int A;
    int B;
};
 
wostream& operator<<(wostream& stream, TwoInts* v)
{
    stream << v->A << L» » << v->B;
    return stream;
}
 
int _pmain(int /*argc*/, _pchar* /*argv*/[])
{
    //// Bad: results in double free.
    //try
    //{
    // TwoInts* p_i = new TwoInts(10, 20);
 
    // auto sp1 = shared_ptr<TwoInts>(p_i);
    // auto sp2 = shared_ptr<TwoInts>(p_i);
    // p_i = nullptr;
 
    // wcout << L»sp1 count is » << sp1.use_count() << L».»
    // L»sp2 count is » << sp2.use_count() << L».»
    //}
    //catch(exception& e)
    //{
    // wcout << L»There was an exception.»
    // wcout << e.what() << endl << endl;
    //}
    //catch(…)
    //{
    // wcout << L»There was an exception due to a double free » <<
    // L»because we tried freeing p_i twice!»
    //}
 
    // This is one right way to create shared_ptrs.
    {
        auto sp1 = shared_ptr<TwoInts>(new TwoInts(10, 20));
        auto sp2 = shared_ptr<TwoInts>(sp1);
 
        wcout << L»sp1 count is » << sp1.use_count() << L».»
            L»sp2 count is » << sp2.use_count() << L».»
 
        wcout << L»sp1 value is » << sp1 << L».»
            L»sp2 value is » << sp2 << L».»
    }
 
    // This is another right way.
    // type as its template argument, and then the argument value(s) to the
    // constructor you want as its parameters, and it automatically
    // constructs the object for you.
    // efficient, as the reference count can be stored with the
    // shared_ptr’s pointed-to object at the time of the object’s creation.
    {
        auto sp1 = make_shared<TwoInts>(10, 20);
        auto sp2 = shared_ptr<TwoInts>(sp1);
 
        wcout << L»sp1 count is » << sp1.use_count() << L».»
            L»sp2 count is » << sp2.use_count() << L».»
 
        wcout << L»sp1 value is » << sp1 << L».»
            L»sp2 value is » << sp2 << L».»
    }
 
    return 0;
}

std::shared_ptr определен в заголовочном файле <memory>.

std::shared_ptr имеет пять функций-членов.

Функция-член get работает так же, как и функция-член std :: unique_ptr :: get.

Функция-член use_count возвращает long, который сообщает вам текущий счетчик ссылок для целевого объекта. Это не включает слабые ссылки.

Уникальная функция-член возвращает bool, сообщая вам, является ли данный конкретный общий указатель единственным владельцем целевого объекта.

Функция-член swap работает так же, как и функция-член std::unique_ptr::swap , с тем добавлением, что подсчет ссылок для ресурсов остается прежним.

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

Функция шаблона std::make_shared — это удобный способ создания исходного std::shared_ptr . Как мы видели ранее в SharedPtrSample , вы передаете тип в качестве аргумента шаблона, а затем просто передаете аргументы, если они есть, для нужного конструктора. std::make_shared создаст экземпляр кучи типа объекта аргумента шаблона и превратит его в std::shared_ptr . Затем вы можете передать этот std::shared_ptr в качестве аргумента конструктору std::shared_ptr чтобы создать больше ссылок на этот общий объект.


Библиотека шаблонов времени выполнения Windows (WRL) предоставляет интеллектуальный указатель с именем ComPtr в пространстве имен Microsoft :: WRL для использования с COM-объектами в приложениях в стиле Metro в Windows 8. Указатель находится в заголовке <wrl / client.h> как часть Windows SDK (минимальная версия 8.0).

Большая часть функциональности операционной системы, которую вы можете использовать в приложениях в стиле Metro, предоставляется средой выполнения Windows («WinRT»). Объекты WinRT предоставляют свои собственные функции автоматического подсчета ссылок для создания и уничтожения объектов. Некоторые системные функции, такие как Direct3D, требуют непосредственного использования и управления им через классический COM. ComPtr обрабатывает подсчет ссылок на основе IUnknown для вас. Он также предоставляет удобные оболочки для QueryInterface и включает в себя другие функциональные возможности, которые полезны для интеллектуальных указателей.

Обычно вы используете две функции-члена As, чтобы получить другой интерфейс для нижележащего COM-объекта, и Get для получения указателя интерфейса на нижележащий COM-объект, который содержит ComPtr (это эквивалентно std::unique_ptr::get ).

Иногда вы будете использовать Detach, который работает так же, как std :: unique_ptr :: release, но имеет другое имя, потому что release в COM подразумевает уменьшение количества ссылок, а Detach этого не делает.

Вы можете использовать ReleaseAndGetAddressOf для ситуаций, когда у вас есть существующий ComPtr, который уже может содержать COM-объект, и вы хотите заменить его новым COM-объектом того же типа. ReleaseAndGetAddressOf делает то же самое, что и функция-член GetAddressOf, но сначала высвобождает базовый интерфейс, если таковой имеется.


В отличие от .NET, где все исключения происходят из System.Exception и имеют гарантированные методы и свойства, исключения C ++ не обязаны происходить из чего-либо; и при этом они даже не обязаны быть типами классов. В C ++ бросьте L «Hello World!»; вполне приемлемо для компилятора, как и throw 5 ;. В принципе, исключения могут быть чем угодно.

Тем не менее, многие программисты на C ++ будут недовольны, увидев исключение, которое не является производным от std::exception (находится в заголовке <exception>). Вывод всех исключений из std::exception обеспечивает способ перехвата исключений неизвестного типа и извлечения информации из них через функцию what перед повторным вызовом. std::exception::what принимает параметров и возвращает const char* string , которую вы можете просмотреть или записать в журнал, чтобы вы знали, что вызвало исключение.

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

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

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

Некоторые программисты используют вариант правила, утверждающего, что вы всегда должны генерировать std::exception определенные в std::exception -derived. Помня, что точка входа (main или wmain) возвращает целое число, эти программисты будут генерировать std::exception derived, когда их код сможет восстановиться, но просто выдают четко определенное целочисленное значение, если сбой неустранимый. Код точки входа будет заключен в блок try, в котором есть зацепка для int. Обработчик catch возвращает возвращенное значение int. В большинстве систем возвращаемое значение 0 из программы означает успех. Любое другое значение означает неудачу.

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

И последнее, на что следует обратить внимание, — это то, что конструкция finally C # не имеет эквивалента в C ++. Идиома RAII, при правильном применении, делает ее ненужной, поскольку все будет очищено.


Мы уже обсуждали std::exception , но в стандартной библиотеке доступно больше типов, чем доступно, и есть дополнительные функциональные возможности для изучения. Давайте сначала посмотрим на функциональность из заголовочного файла <exception>.

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

Чтобы установить пользовательский терминатор, вы вызываете std::set_terminate и передаете ему адрес функции. Вы можете изменить пользовательский обработчик завершения в любое время; последний набор функций — это то, что будет вызываться в случае вызова std::terminate или необработанного исключения. Обработчик по умолчанию вызывает функцию прерывания из заголовочного файла <cstdlib>.

Заголовок <stdexcept> обеспечивает элементарную структуру для исключений. Он определяет два класса, которые наследуются от std::exception . Эти два класса служат родительским классом для нескольких других классов.

Класс std::runtime_error является родительским классом для исключений, std::runtime_error средой выполнения или из-за ошибки в функции стандартной библиотеки C ++. Его потомками являются класс std::overflow_error класс std::range_error класс std::range_error .

Класс std::logic_error является родительским классом для исключений, std::logic_error из-за ошибки программиста. Его потомками являются класс std::domain_error класс std::invalid_argument класс std::length_error класс std::out_of_range .

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

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

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

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

Образец: ExceptionsSample \ InvalidArgumentException.h

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#pragma once
#include <exception>
#include <stdexcept>
#include <string>
#include <sstream>
 
namespace CppForCsExceptions
{
    class InvalidArgumentExceptionBase :
        public std::invalid_argument
    {
    public:
        InvalidArgumentExceptionBase(void) :
            std::invalid_argument(«») { }
 
        virtual ~InvalidArgumentExceptionBase(void) throw() { }
 
        virtual const char* what(void) const throw() override = 0;
    };
 
    template <class T>
    class InvalidArgumentException :
        public InvalidArgumentExceptionBase
    {
    public:
        inline InvalidArgumentException(
            const char* className,
            const char* functionSignature,
            const char* parameterName,
            T parameterValue
            );
 
        inline virtual ~InvalidArgumentException(void) throw();
 
        inline virtual const char* what(void) const throw() override;
 
    private:
        std::string m_whatMessage;
    };
 
    template<class T>
    InvalidArgumentException<T>::InvalidArgumentException(
        const char* className,
        const char* functionSignature,
        const char* parameterName,
        T parameterValue) : InvalidArgumentExceptionBase(),
        m_whatMessage()
    {
        std::stringstream msg;
        msg << className << «::» << functionSignature <<
            » — parameter ‘» << parameterName << «‘ had invalid value ‘» <<
            parameterValue << «‘.»;
        m_whatMessage = std::string(msg.str());
    }
 
    template<class T>
    InvalidArgumentException<T>::~InvalidArgumentException(void) throw()
    { }
 
    template<class T>
    const char* InvalidArgumentException<T>::what(void) const throw()
    {
        return m_whatMessage.c_str();
    }
}

Образец: ExceptionsSample \ ExceptionsSample.cpp

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#include <iostream>
#include <ostream>
#include <memory>
#include <exception>
#include <stdexcept>
#include <typeinfo>
#include <algorithm>
#include <cstdlib>
#include «InvalidArgumentException.h»
#include «../pchar.h»
 
using namespace CppForCsExceptions;
using namespace std;
 
class ThrowClass
{
public:
    ThrowClass(void)
        : m_shouldThrow(false)
    {
        wcout << L»Constructing ThrowClass.»
    }
 
    explicit ThrowClass(bool shouldThrow)
        : m_shouldThrow(shouldThrow)
    {
        wcout << L»Constructing ThrowClass. shouldThrow = » <<
            (shouldThrow ? L»true.» : L»false.») << endl;
        if (shouldThrow)
        {
            throw InvalidArgumentException<const char*>(
                «ThrowClass»,
                «ThrowClass(bool shouldThrow)»,
                «shouldThrow»,
                «true»
                );
        }
    }
 
    ~ThrowClass(void)
    {
        wcout << L»Destroying ThrowClass.»
    }
 
    const wchar_t* GetShouldThrow(void) const
    {
        return (m_shouldThrow ? L»True» : L»False»);
    }
 
private:
    bool m_shouldThrow;
};
 
class RegularClass
{
public:
    RegularClass(void)
    {
        wcout << L»Constructing RegularClass.»
    }
    ~RegularClass(void)
    {
        wcout << L»Destroying RegularClass.»
    }
};
 
class ContainStuffClass
{
public:
    ContainStuffClass(void) :
        m_regularClass(new RegularClass()),
        m_throwClass(new ThrowClass())
    {
        wcout << L»Constructing ContainStuffClass.»
    }
 
    ContainStuffClass(const ContainStuffClass& other) :
        m_regularClass(new RegularClass(*other.m_regularClass)),
        m_throwClass(other.m_throwClass)
    {
        wcout << L»Copy constructing ContainStuffClass.»
    }
 
    ~ContainStuffClass(void)
    {
        wcout << L»Destroying ContainStuffClass.»
    }
 
    const wchar_t* GetString(void) const
    {
        return L»I’m a ContainStuffClass.»;
    }
 
private:
 
    unique_ptr<RegularClass> m_regularClass;
    shared_ptr<ThrowClass> m_throwClass;
};
 
void TerminateHandler(void)
{
    wcout << L»Terminating due to unhandled exception.»
 
    // If you call abort (from <cstdlib>), the program will exit
    // abnormally.
    // anything to cause it to exit from this method.
    abort();
 
    //// If you were instead to call exit(0) (also from <cstdlib>),
    //// then your program would exit as though nothing had
    //// gone wrong.
    //// I present this so you know that it is possible for
    //// a program to throw an uncaught exception and still
    //// exit in a way that isn’t interpreted as a crash, since
    //// you may need to find out why a program keeps abruptly
    //// exiting yet isn’t crashing.
    //// for that.
    //exit(0);
}
 
int _pmain(int /*argc*/, _pchar* /*argv*/[])
{
    // Set a custom handler for std::terminate.
    // won’t run unless you run it from a command prompt.
    // will intercept the unhandled exception and will present you with
    // debugging options when you run it from Visual Studio.
    set_terminate(&TerminateHandler);
 
    try
    {
        ContainStuffClass cSC;
        wcout << cSC.GetString() << endl;
 
        ThrowClass tC(false);
        wcout << L»tC should throw? » << tC.GetShouldThrow() << endl;
 
        tC = ThrowClass(true);
        wcout << L»tC should throw? » << tC.GetShouldThrow() << endl;
    }
    // One downside to using templates for exceptions is that you need a
    // catch handler for each specialization, unless you have a base
    // class they all inherit from, that is.
    // other std::invalid_argument exceptions, we created an abstract
    // class called InvalidArgumentExceptionBase, which serves solely to
    // act as the base class for all specializations of
    // InvalidArgumentException<T>.
    // without needing a catch handler for each.
    // you could still have a handler for a particular specialization.
    catch (InvalidArgumentExceptionBase& e)
    {
        wcout << L»Caught ‘» << typeid(e).name() << L»‘.»
            L»Message: » << e.what() << endl;
    }
    // Catch anything derived from std::exception that doesn’t already
    // have a specialized handler.
    // should catch it, log it, and re-throw it.
    catch (std::exception& e)
    {
        wcout << L»Caught ‘» << typeid(e).name() << L»‘.»
            L»Message: » << e.what() << endl;
        // Just a plain throw statement like this is a re-throw.
        throw;
    }
    // This next catch catches everything, regardless of type.
    // catching System.Exception, you should only catch this to
    // re-throw it.
    catch (…)
    {
        wcout << L»Caught unknown exception type.»
        throw;
    }
 
    // This will cause our custom terminate handler to run.
    wcout << L»tC should throw? » <<
        ThrowClass(true).GetShouldThrow() << endl;
 
    return 0;
}

Хотя я упоминал об этом в комментариях, я просто хотел еще раз указать, что пользовательская функция завершения не будет работать, пока вы не запустите этот пример из командной строки. Если вы запустите его в Visual Studio, отладчик перехватит программу и организует собственное завершение после того, как даст вам возможность проверить состояние, чтобы увидеть, можете ли вы определить, что пошло не так. Также обратите внимание, что эта программа всегда будет аварийно завершать работу. Это сделано специально, так как позволяет увидеть обработчик завершения в действии.

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

Этот урок представляет собой главу из C ++ Succinctly , бесплатной книги от команды Syncfusion .