Категории
Самые читаемые
Лучшие книги » Компьютеры и Интернет » Программирование » Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Читать онлайн Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 61 62 63 64 65 66 67 68 69 ... 73
Перейти на страницу:

// вызывается глобальный обработчик 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 ограничена, поскольку эта форма применимо только для выделения памяти; последующие вызовы конструктора могут по-прежнему возбуждать исключения.

Правило 50: Когда имеет смысл заменять new и delete

Вернемся к основам. Прежде всего зачем кому-то может понадобиться подменять предлагаемые компилятором версии operator new и operator delete? Существуют, по крайней мере, три распространенные причины.

Чтобы обнаруживать ошибки применения. Если не освобождать (с помощью оператора delete) память, выделенную оператором new, это приведет к утечкам памяти. Вызов delete более одного раза для одного и того же участка памяти, выделенного new, приведет к неопределенному поведению программы. Если оператор new будет вести список выделенных адресов, а оператор delete удалять адреса освобожденных областей из списка, то такие ошибки легко обнаружить. Аналогично различные ошибки в программе могут приводить к записи за концом выделенного блока (переполнению буфера) либо с адреса, предшествующего началу выделенного блока. Специализированная версия оператора new может запрашивать блоки большего размера и в неиспользуемое место до и после области, доступной пользователям, записывать некоторую комбинацию битов («сигнатуры»). При этом оператор delete может проверять наличие такой сигнатуры. Если ее нет, значит, память была затерта, и оператор delete может запротоколировать этот факт вместе со значением указателя, для которого это обнаружилось.

Чтобы повысить эффективность. Версии операторов new и delete, поставляемые вместе с компилятором, универсальны. Они должны быть приемлемы как для долго работающих программ (например, Web-серверов), так и для программ, работающих менее одной секунды. Они должны уметь обрабатывать серии запросов на выделение больших блоков памяти, малых блоков, а также смеси тех и других. Они должны адаптироваться к широкому диапазону вариантов использования – от динамического выделения нескольких блоков большого размера, которые существуют на протяжении всего времени работы программы, до выделения и освобождения памяти для большого количества мелких объектов с малым временем жизни. Они должны предотвращать фрагментацию «кучи», ибо если этого не делать, то в конце концов будет невозможно удовлетворить запрос на выделение большого блока памяти, даже если суммарно такой объем имеется, но разнесен по множеству мелких участков.

1 ... 61 62 63 64 65 66 67 68 69 ... 73
Перейти на страницу:
На этой странице вы можете бесплатно скачать Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс торрент бесплатно.
Комментарии