Правило 49: Разберитесь в поведении обработчика new
Правило 49: Разберитесь в поведении обработчика new
Когда оператор new не может удовлетворить запрос на выделение памяти, он возбуждает исключение. Когда-то он возвращал нулевой указатель, и некоторые старые компиляторы все еще так и поступают. Вы можете столкнуться с таким устаревшим поведением, но я отложу его обсуждение до конца правила.
Прежде чем возбудить исключение в ответ на невозможность удовлетворить запрос на выделение памяти, оператор new вызывает определенную пользователем функцию, называемую обработчиком new (new-handler). (На самом деле это не совсем так. Реальное поведение new несколько сложнее. Подробности описаны в правиле 51.) Чтобы задать функцию, обрабатывающую нехватку памяти, клиенты вызывают set_new_handler – стандартную библиотечную функцию, объявленную в заголовочном файле <new>:
namespace std {
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
}
Как видите, new_handler – это typedef для указателя на функцию, которая ничего не принимает и не возвращает, а set_new_handler – функция, которая принимает и возвращает new_handler (конструкция throw() в конце объявления set_new_handler – это спецификация исключения; по существу, она сообщает, что функция не возбуждает никаких исключений, хотя на самом деле все несколько интереснее; подробности см. в правиле 29).
Параметр set_new_handler – это указатель на функцию, которую operator new вызывает при невозможности выделить запрошенную память. Возвращаемое set_new_handler значение – указатель на функцию, которая использовалась для этой цели перед вызовом set_new_handler.
Используется set_new_handler следующим образом:
// функция, которая должна быть вызвана, если operator new
// не может выделить память
void outOfMem()
{
std::cerr << “Невозможно удовлетворить запрос на выделение памяти ”;
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int *pBigDataArray = new int[100000000L];
...
}
Если operator new не может выделить память для размещения 100 000 000 целых чисел, будет вызвана функция outOfMem, и программа завершится, выдав сообщение об ошибке. (Кстати, подумайте, что случится, если память должна быть динамически выделена во время вывода сообщения об ошибке в cerr…)
Когда operator new не может удовлетворить запрос в памяти, он будет вызывать обработчик new до тех пор, пока он не сумеет найти достаточно памяти. Код, приводящий к повторным вызовам, показан в правиле 51, но и такого высокоуровневого описания достаточно, чтобы сделать вывод о том, что правильно спроектированная функция-обработчик new должна делать что-то одно из следующего списка:
• Найти дополнительную память. В результате следующая попытка выделить память внутри operator new может завершиться успешно. Один из способов реализовать такую стратегию – выделить большой блок памяти в начале работы программы, в затем освободить его при первом вызове обработчика new.
• Установить другой обработчик new. Если текущий обработчик не может выделить память, то, возможно, ему известен какой-то другой, который сможет это сделать. Если так, то текущий обработчик может установить вместо себя другой (вызвав set_new_handler). В следующий раз, когда operator new обратится к обработчику, будет вызван последний установленный. (В качестве альтернативы обработчик может изменить собственное поведение, чтобы при следующем вызове сделать что-то другое. Добиться этого можно, например, путем модификации некоторых статических, определенных в пространстве имен или глобальных данных, влияющих на его поведение.)
• Убрать обработчик new, то есть передать нулевой указатель set_new_handler. Если обработчик не установлен, то operator new сразу возбудит исключение при неудачной попытке выделить память.
• Возбудить исключение типа bad_alloc либо некоторого типа, унаследованного от bad_alloc. Такие исключения не перехватывает operator new, поэтому они распространяются до того места, где была запрошена память.
• Не возвращать управление – обычно вызвав abort или exit.
Эти варианты выбора обеспечивают вам достаточную гибкость в реализации функций-обработчиков new.
Иногда обработать ошибки при выделении памяти можно и другими способами, зависящими от класса распределяемого объекта:
class X {
public:
static void outOfMemory();
...
};
class Y {
public:
static void outOfMemory();
...
};
X *p1 = new X; // если выделить память не удалось,
// вызвать X::outOfMemory
Y *p2 = new Y; // если выделить память не удалось,
// вызвать Y::outOfMemory
С++ не поддерживает специфичных для класса обработчиков new, но он и не нуждается в них. Вы можете реализовать такое поведение самостоятельно. Для этого просто в каждом классе определяете собственную версию set_new_handler и operator new. Определенная в классе функция set_new_handler класса позволит пользователям задать обработчик new для класса (точно так же, как обычный set_new_handler устанавливает глобальный обработчик new). Принадлежащий классу operator new гарантирует, что при выделении памяти для объектов этого класса вместо глобального обработчика new будет использован тот, что определен в данном классе.
Предположим, вы хотите обработать ошибки выделения памяти для класса Widget. Понадобится функция, которая будет вызываться, когда operator new не может выделить достаточно памяти для объекта Widget, поэтому вы объявляете статический член типа new_handler для хранения указателя на обработчик new для класса. Тогда Widget будет выглядеть примерно так:
class Widget {
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void *operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
};
Статические члены класса должны быть определены вне самого класса (если только они не константные целые – см. правило 2), поэтому:
std::new_handler Widget::currentHandler = 0; // инициализировать нулем
// в файле реализации класса
Функция set_new_handler в классе Widget сохранит переданный ей указатель и вернет тот указатель на функцию, действовавшую ранее. Так же поступает и стандартная версия set_new_handler:
static std::new_handler set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
А вот что должен делать operator new из класса Widget.
1. Вызвать стандартный set_new_handler, указав в качестве параметра функцию-обработчик ошибок из класса Widget. В результате обработчик new из класса Widget будет установлен в качестве глобального.
2. Вызвать глобальный operator new для реального выделения памяти. Если произойдет ошибка, глобальный operator new вызовет обработчик new, принадлежащий Widget, поскольку эта функция была установлена в качестве глобального обработчика. Если это ни к чему не приведет, то глобальный operator new возбудит исключение bad_alloc. В этом случае operator new из класса Widget должен восстановить исходный обработчик new, а затем распространить исключение. Чтобы гарантировать, что исходный обработчик всегда восстанавливается, класс Widget трактует его как ресурс и следует совету правила 13 об использовании управляющих ресурсами объектов для предотвращения утечек.
3. Если глобальный operator new в состоянии выделить достаточно памяти для объекта Widget, то operator new класса Widget возвращает указатель на выделенную память. Деструктор объекта, самостоятельно управляющего глобальным обработчиком new, автоматически восстанавливает тот глобальный обработчик, который был установлен перед вызовом operator new класса Widget.
Теперь посмотрим, как все это выразить на C++. Начнем с класса, управляющего ресурсами, который не содержит ничего, кроме основных операций, диктуемых идиомой RAII: захват ресурса во время конструирования объекта и освобождение при его уничтожении (см. правило 13):
class NewHandlerHolder {
public:
explicit NewHandlerHolder(std::new_handler nh) // получить текущий
:handler(nh) {} // обработчик new
~NewHandlerHolder() // освободить его
{ std::set_new_handler(handler);}
private:
std::new_handler handler; // запомнить его
NewHandlerHolder(const NewHandlerHolder&); // предотвратить
NewHandlerHolder& // копирование
operator=(const NewHandlerHolder&); // (см. правило 14)
};
Это делает реализацию оператора new для Widget совсем простой:
void Widget::orerator new(std::size_td size) throw(std::bad_aloc)
{
NewHandlerHolder // установить обработчик
h(std::set_new_handler(currentHandler)); // new из класса Widget
return ::operator new(size); // выделить память или
// возбудить исключение
} // восстановить глобальный
// обработчик new
Пользователи класса Widget применяют эти средства следующим образом:
void outOfMem(); // объявление функции, которую нужно
// вызвать, если выделить память
// для Widget не удается
Widget::set_new_handler(outOfmem); // установка outOfMem в качестве
// обработчика new для Widget
Widget *pw1 = new Widget; // если выделить память не удалось,
// вызывается outOfMem
std::string *ps = new std::string; // если выделить память не удалось,
// вызывается глобальный обработчик new
// (если есть)
Widget::set_new_handler(0); // отменяет обработчик new
Widget *pw1 = new Widget; // если выделить память не удалось,
// сразу же возбуждается исключение (никакого
// обработчика new сейчас нет)
Код, реализующий эту схему, один и тот же (независимо от класса), поэтому разумно было бы повторно использовать его в других местах. Простой способ сделать это – создать «присоединяемый» базовый класс, то есть базовый класс, который предназначен для того, чтобы подклассы могли унаследовать одно-единственное средство, в данном случае способность устанавливать специфичный для класса обработчик new. Затем превратите базовый класс в шаблон, чтобы каждый производный класс мог получать разные копии данных.
При таком подходе принадлежащая базовому классу часть позволяет подклассам наследовать необходимые им функции set_new_handler и operator new, а шаблонная часть гарантирует, что у каждого подкласса будет собственный член данных currentHandler. Звучит сложновато, но код выглядит обнадеживающе знакомым. Фактически единственным отличием является то, что теперь он доступен любому классу:
template<typename T> // «присоединяемый» базовый класс для
class NewHandlerSupport { // поддержки специфичной для класса
public: // функции set_new_handler
static std::new_handler set_new_handler(std::new_handler p) throw();
static void *operator new(std::size_t size) throw(std::bad_alloc);
... // другие версии оператора new – см. правило 52
private:
static std::new_handler currentHandler;
};
template<typename T>
std::new_handler
NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void *NewHandlerSupport<T>::operator(std::size_t size)
throw(std::bad_alloc)
{
NewHandlerHolder h(std::set_new_handler(currentHandler);
return ::operator new(size);
}
// currentHandler в любом классе инициализируется значением null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
С этим шаблоном класса добавление поддержки set_new_handler к Widget очень просто: Widget просто наследуется от NewHandlerSupport<Widget>. (Это может показаться экстравагантным, но ниже я подробно объясню, что здесь происходит.)
class Widget: public NewHandlerSupport<Widget> {
... // как раньше, но без декларации
}; // set_new_handler или operator new
Это все, что нужно сделать в классе Widget, чтобы предоставить специфичный для класса обработчик set_new_handler.
Но может быть, вас беспокоит тот факт, что Widget наследует классу New-HandlerSupport<Widget>? Если так, то ваше беспокойство усилится, когда вы заметите, что NewHandlerSupport никогда не использует свой параметр типа T. Он не нуждается в нем. Единственное, что нам нужно, – это отдельная копия NewHandlerSupport, а точнее его статический член currentHandler для каждого класса, производного от NewHandlerSupport. Параметр шаблона T просто отличает один подкласс от другого. Механизм шаблонов автоматически генерирует копию currentHandler для каждого типа T, для которого он конкретизируется.
А что касается того, что Widget наследует шаблонному базовому классу, который принимает Widget как параметр типа, не пугайтесь, это только поначалу кажется непривычным. На практике это очень удобная техника, имеющая собственное название, которое отражает тот факт, что никому из тех, кто видит ее в первый раз, она не кажется естественной. А называется она курьезный рекурсивный шаблонный паттерн (curious recurring template pattern – CRTP). Честное слово.
Однажды я опубликовал статью, в которой писал, что лучше было бы это назвать ее «Сделай Это Для Меня», потому что Widget наследует NewHandler-Support<Widget> и как бы говорит: «Я – Widget, и я хочу наследовать классу NewHandlerSupport для Widget». Никто не станет пользоваться предложенным мной названием (даже я сам), но если думать о CRTP как о способе сказать «сделай это для меня», то вам будет проще понять смысл наследование шаблону.
Шаблоны, подобные NewHandlerSupport, упрощают добавление специфичных для класса обработчиков new к любому классу, которому это нужно. Однако наследование присоединяемому классу приводит к множественному наследованию, и прежде чем вставать на этот путь, вам, возможно, стоит перечитать правило 40.
До 1993 года C++ требовал, чтобы оператор new возвращал нулевой указатель, если не мог выделить нужную память. Теперь же operator new в этом случае должен возбуждать исключение bad_alloc. Но огромный объем кода на C++ был написан до того, как компиляторы стали поддерживать новую спецификацию. Комитет по стандартизации C++ не хотел отметать весь код, основанный на сравнении с null, поэтому было решено предоставить альтернативные формы operator new, обеспечивающие традиционное поведение с возвращением null при неудаче. Эти формы известны под названием «nothrow» (не возбуждающие исключений), потому что в них используются объекты nothrow (определенные в заголовке <new>) в точке, где используется new:
class Widget {...};
Widget *pw1 = new Widget; // возбуждает bad_alloc, если
// выделить память не удалось
if(pw1 == 0) ... // эта проверка должна
// завершиться неудачно
Widget *pw2 = new (std::nothrow)Widget; // возвращает 0, если выделить
// память не удалось
if(pw2 == 0) ... // эта проверка может
// завершиться успешно
Не возбуждающий исключений оператор new предоставляет менее надежные гарантии относительно исключений, чем кажется на первый взгляд. В выражении «new (std::nothrow)Widget» происходят две вещи. Во-первых, nothrow-версия оператора new вызывается для выделения памяти, достаточной для размещения объекта Widget. Если получить память не удалось, то оператор new возвращает нулевой указатель, как нам и хотелось. Если же память выделить удалось, то вызывается конструктор Widget, и в этой точке все гарантии заканчиваются. Конструктор Widget может делать все, что угодно. Он может сам по себе запросить с помощью new какую-то память, и если он это делает, никто не заставляет его использовать nothrow. Хотя вызов оператора new в «new (std::nothrow)Widget» не возбуждает исключений, к конструктору Widget это не относится. И если он возбудит исключение, то оно будет распространяться как обычно. Вывод? Применение nothrow new гарантирует только то, что данный operator new не возбудит исключений, но не дает никаких гарантий относительно выражения, подобного «new (std::nothrow)Widget». А потому вряд ли стоит вообще прибегать к nothrow new.
Независимо от того, используете вы «нормальный» (возбуждающий исключения) new или же вариант nothrow, важно, чтобы вы понимали поведение обработчика new, поскольку он вызывается обеими формами.
Что следует помнить
• set_new_handler позволяет указать функцию, которая должна быть вызвана, если запрос на выделение памяти не может быть удовлетворен.
• Полезность nothrow new ограничена, поскольку эта форма применимо только для выделения памяти; последующие вызовы конструктора могут по-прежнему возбуждать исключения.
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКДанный текст является ознакомительным фрагментом.
Читайте также
Выполнение обработчика завершения и выход из него
Выполнение обработчика завершения и выход из него Обработчик завершения, или блок __finally, выполняется в контексте блока или функции, работу которых он отслеживает. Управление может переходить от оператора завершения к следующему оператору. Существует и другая
Регистрация обработчика прерывания
Регистрация обработчика прерывания Ответственность за обработчики прерываний лежит на драйверах устройств, которые управляют определенным типом аппаратного обеспечения. С каждым устройством связан драйвер, и если устройство использует прерывания (а большинство
Освобождение обработчика прерывания
Освобождение обработчика прерывания Для освобождения линии прерывания необходимо вызвать функциюvoid free_irq(unsigned int irq, void *dev_id);Если указанная линия не является совместно используемой, то эта функция удаляет обработчик и запрещает линию прерывания. Если линия запроса на
Написание обработчика прерывания
Написание обработчика прерывания Следующее описание является типичным для обработчика прерывания.static irqreturn_t intr_handler(int irq, void *dev_id, struct pt_regs *regs);Заметим, что оно должно соответствовать аргументу, который передается в функцию request_irq(). Первый параметр, irq, — это численное
Отключение обработчика прерывания
Отключение обработчика прерывания Когда вы закончили с обработчиком прерывания, вы можете пожелать уничтожить связь между ним и вектором:int InterruptDetach(int id);Я сказал «можете», потому что обрабатывающие прерывания потоки, как правило, используются в серверах, а серверы
Установка пользовательского обработчика ошибок
Установка пользовательского обработчика ошибок set_error_handlerУстановка пользовательского обработчика ошибок.Синтаксис:string set_error_handler(string error_handler)Функция возвращает имя функции, ранее определенной в качестве обработчика ошибок (или FALSE при ошибке), и устанавливает, в
Правило большинства
Правило большинства При планировании сайта исходите из того, что ваш клиент – начинающий пользователь, а не компьютерный гений. В этом и заключается правило большинства.Компьютерный гений при покупке товара вообще не обратит внимания на удобство сайта, так что его
Пример: уведомление с использованием sigwait вместо обработчика
Пример: уведомление с использованием sigwait вместо обработчика Хотя программа из предыдущего примера работает правильно, можно повысить ее эффективность. Программа использует sigsuspend для блокировки в ожидании прихода сообщения. При помещении сообщения в пустую очередь
Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции
Правило 41: Разберитесь в том, что такое неявные интерфейсы и полиморфизм на этапе компиляции В мире объектно-ориентированного программирования преобладают явные интерфейсы и полиморфизм на этапе исполнения. Например, рассмотрим следующий (бессмысленный) класс:class Widget
Доступ к объекту Graphics вне обработчика Paint
Доступ к объекту Graphics вне обработчика Paint В некоторых редких случаях может понадобиться доступ к объекту Graphics вне контекста обработчика события Paint. Предположим, например, что нужно перерисовать небольшой круг с центром в точке (х, у), где был выполнен щелчок кнопки мыши.
Совет 41. Разберитесь, для чего нужны ptr_fun, mem_fun и mem_fun_ref
Совет 41. Разберитесь, для чего нужны ptr_fun, mem_fun и mem_fun_ref Загадочные функции ptr_fun/mem_fun/mem_fun_ref часто вызывают недоумение. В одних случаях их присутствие обязательно, в других они не нужны... но что же они все-таки делают? На первый взгляд кажется, что они бессмысленно
Разберитесь с долгами
Разберитесь с долгами Расплачивайтесь по долгам вашего кода и дизайнаМы обычно говорим о долгах в денежном выражении, но долги могут принимать и другие формы. Вы можете быстро обрасти долгами кода и дизайна.Наваяли блок кода, который функционален, но все еще неопрятен —
Правило третей
Правило третей Правило третей – это упрощенный вариант правила золотого сечения. Примечание Чтобы получить золотое сечение отрезка, его разбивают на две части таким образом, что соотношение его меньшей части к большей равно соотношению большей части ко всему отрезку.
Правило типизации
Правило типизации Правило типизации, делающее допустимым первый набор и недопустимым второй, интуитивно понятно, но его надо уточнить.Вначале рассмотрим обычные, не родовые классы. Пусть C такой класс. Рассмотрим объявление его компонента, не использующее, естественно,