Правило 51: Придерживайтесь принятых соглашений при написании new и delete

Правило 51: Придерживайтесь принятых соглашений при написании new и delete

В правиле 50 объясняется, зачем могут понадобиться собственные версии операторов new и delete, но ничего не говорится о соглашениях, которых следует придерживаться при их написании. Следовать этим соглашениям не так уж сложно, но некоторые из них противоречат интуиции, поэтому знать о них необходимо.

Начнем с оператора new. От отвечающего стандарту оператора new требуется, чтобы он возвращал правильное значение, вызывал обработчика new, когда запрошенную память не удается выделить (см. правило 49), и правильно обрабатывал запросы на выделения нуля байтов. Кроме того, надо принять меры к тому, чтобы нечаянно не скрыть «нормальную» форму new, хотя это в большей мере касается интерфейса класса, чем требований реализации (см. правило 52).

Обеспечить правильность возвращаемого оператором new значения легко. Если вы можете выделить запрошенную память, то возвращаете указатель на нее. Если не можете, то следуете рекомендациям из правила 49 и возбуждаете исключение типа bad_alloc.

Однако все не так просто, потому что оператор new пытается выделить память не один раз, и после каждой неудачи вызывает функцию-обработчик new. Предполагается, что это сможет что-то сделать для освобождения некоторого объема памяти. Только тогда, когда указатель на обработчик new равен нулю, оператор new возбуждает исключение.

Забавно, но C++ требует, чтобы оператор new возвращал корректный указатель даже тогда, когда запрошено 0 байтов памяти. (Такое странное поведение упрощает реализацию некоторых вещей в других местах языка.) С учетом этого случая псевдокод для оператора new (нечлена класса) выглядит так:

void *operator new(std::size_t size) throw(std::bad_alloc)

{ // ваш оператор new может принимать

using namespace std; // дополнительные параметры

if (size == 0) { // обработать запрос на 0 байтов,

size = 1; // считая, что нужно выделить 1 байт

}

while(true) {

попытка выделить size байтов;

if(выделить удалось)

return (указатель на память);

// выделить память не удалось; проверить, установлена ли

// функция-обработчик new (см. ниже)

new_handler globalHandler = set_new_handler(0);

set_new_handler(globalHandler);

if(globalHandler) (*globalHandler)();

else throw std::bad_alloc();

}

}

Трактовка запроса на 0 байтов так, как если бы запрашивался 1 байт, выглядит сомнительно, но это просто, это корректно, это работает, к тому же на сколько часто вы собираетесь запрашивать 0 байтов?

Вам также может не понравиться то место в псевдокоде, где указатель на функцию-обработчик устанавливается в нуль, а затем восстанавливается его прежнее значение. К сожалению, нет способа непосредственно получить указатель на текущий обработчик new, поэтому приходится вызывать set_new_handler, чтобы получить его текущее значение. Грубо, но тоже эффективно, по крайней мере, в однопоточной программе. В многопоточной среде, возможно, понадобится какой-то механизм синхронизации для безопасного манипулирования (глобальными) структурами данных, связанными с функцией-обработчиком new.

В правиле 49 отмечено, что оператор new содержит бесконечный цикл, и в приведенном выше коде этот цикл присутствует: «while(true)». Единственный способ выйти из цикла – успешно выделить память либо выполнить в функции-обработчике одно из описанных в правиле 49 действий: сделать доступной больше памяти, установить другой обработчик, убрать текущий обработчик, возбудить исключение типа, производного от bad_alloc, либо не возвращать управления вовсе. Теперь вам должно быть ясно, почему обработчик new должен вести себя подобным образом. Если он нарушит это соглашение, то цикл внутри оператора new никогда не завершится.

Многие не понимают, что функция-член operator new наследуется производными классами. Это может привести к некоторым интересным осложнениям. Заметьте, что в приведенном псевдокоде operator new производится попытка выделить size байтов (если только size не равно нулю). Естественно, ведь size – это аргумент, переданный функции. Однако, как объясняется в правиле 50, одной из причин написания специального менеджера памяти является оптимизация размещения объектов определенного класса, но не любых его подклассов. Иными словами, если в классе X определен оператор new, то предполагается, что он рассчитан на объекты размера sizeof(X) – ни больше, ни меньше. Из-за наследования, однако, появляется возможность вызвать оператор new базового класса, чтобы выделить память для объекта производного класса:

class Base {

public:

static void *operator new(std::size_t size) throw(std::bad_alloc);

...

};

class Derived: public Base // в подклассе не объявлен operator new

{...};

Derived *p = new Derived; // вызывается Base::operator new!

Если определенный в классе Base оператор new не был спроектирован с учетом этой проблемы (а такая вероятность есть), то для корректной работы в случае, когда поступил запрос на выделение памяти «неправильного» размера, лучше всего обратиться к стандартному оператору new:

void *Base::operator new(std::size_t) throw(std::bad_alloc)

{

if(size != sizeof(Base)) // если size «неправильный»

return ::operator new(size); // вызвать стандартный оператор new

// для обработки запроса

... // в противном случае обработать запрос

// здесь

}

Я слышу возгласы: «Подождите! Вы забыли проверить патологический случай с нулевым размером!» На самом деле нет. Проверка присутствует, просто она является частью сравнения с sizeof(Base). C++ иногда предъявляется странные требования, например все автономные объекты должны иметь ненулевой размер (см. правило 39). По определению, sizeof(Base) никогда не может вернуть нуль, поэтому если size равно нулю, то запрос будет переадесован::operator new, и обязанность правильно обработать запрос возлагается на него.

Если вы хотите управлять распределением памяти для массивов на уровне класса, то нужно будет реализовать оператор new[] – специально для массивов. (Эта функция обычно называется «new для массивов», потому что трудно представить, как надо произносить «operator new[]»). Если вы решите написать operator new[], то помните, что она должна лишь выделить блок неформатированной памяти. Вы не можете ничего делать с еще не существующими объектами в этом массиве. Фактически вы даже не можете определить, сколько объектов будет в этом массиве. Во-первых, вы не знаете размер объекта. А ведь из-за наследования может быть вызван оператор new[] базового класса для выделения памяти под массив объектов производного класса, которые обычно больше объектов базового класса. Поэтому вы не можете предполагать внутри Base::operator new[], что размер каждого объекта равен sizeof(Base), а значит, нельзя предполагать, что общее количество объектов в массиве будет равно (запрошенное число байmов)/sizeof(Base). Во-вторых, параметр size_t, переданный оператору new[], может соответствовать большему объему памяти, чем займут сами объекты, потому что, как объясняется в правиле 16, в динамически выделенных массивах может резервироваться место для хранения числа элементов массива.

Это все соглашения, которым вы должны следовать при написании оператора new[]. Что касается оператора delete, то с ним все проще. Почти все, что вам нужно знать, – это то, что C++ гарантирует безопасность освобождения памяти по нулевому адресу, поэтому и вы должны предоставить такую гарантию. Вот псевдокод для оператора delete, не являющегося членом класса:

void operator delete(void *rawMemory) throw()

{

if(rawMemory == 0) return; // ничего не делать, если передан нулевой

// указатель

освободить память, на которую указывает rawMemory

;

}

Версия этой функции, являющаяся членом класса, также проста, за исключением того, что нужно проверить размер того, что вы собираетесь освобождать. Предполагая, что оператор new, определенный в классе, передает запрос на выделение «неправильного» количества байтов глобальному::operator new, вы также должны передать информацию о «неверном» размере функции::operator delete:

class Base { // то же, что и раньше, но добавлено

public: // объявление operator delete

static void *operator new(std::size_t size) throw(std::bad_alloc);

static void operator delete(void *rawMemory, std::size_t size) throw();

...

};

void Base::operator delete(void *rawMemory, std::size_t size) throw()

{

if(rawMemory == 0) return; // проверка на нулевой указатель

if(size != sizeof(Base)) { // если размер «неверный»,

::operator delete(rawMemory); // вызвать стандартный оператор

return; // delete для обработки запроса

}

освободить память, на которую указывает rawMemory

;

return;

}

Интересно, что значение типа size_t, которое C++ передает оператору delete, может быть неправильным, если удаляется объект, производный от класса, в котором нет виртуального деструктора. Одного этого уже достаточно, чтобы требовать от базового класса наличия виртуального деструктора, но в правиле 7 описана и другая, более существенная причина. Пока просто отметьте, что если вы опустили виртуальный деструктор в базовом классе, то функция operator delete может работать неправильно.

Что следует помнить

• Оператор new должен содержать бесконечный цикл, который пытается выделить память, должен вызывать функцию-обработчик new, если не удается удовлетворить запрос на выделение памяти, и должен обрабатывать запрос на выделение нуля байтов. Версии оператора new уровня класса должны обрабатывать запросы на выделение блоков большего размера, чем ожидается.

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

Данный текст является ознакомительным фрагментом.



Поделитесь на страничке

Следующая глава >

Похожие главы из других книг:

DELETE

Из книги автора

DELETE DELETE FROM <table name> [ WHERE <predicate> | WHERE CURRENT OF <cursor name> (*только для вложения*) ];Элементы, используемые в командах МОДИФИКАЦИИ ЭЛЕМЕНТ ОПРЕДЕЛЕНИЕ <cursor name> Имя курсора используемого в этой программе. <query> Допустимая команда SELECT. Для других элементов


R.5.3.4 Операция delete

Из книги автора

R.5.3.4 Операция delete Операция delete уничтожает объект, созданный с помощью new.выражение-освобождения: ::opt delete выражение-приведения ::opt delete [] выражение-приведенияРезультат имеет тип void. Операндом delete должен быть указатель, который возвращает new. Эффект применения операции delete


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

Из книги автора

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


19.2.4.3. Придерживайтесь стандартной практики именования файлов

Из книги автора

19.2.4.3. Придерживайтесь стандартной практики именования файлов Еще до просмотра README-файла бесстрашный исследователь внимательно изучит имена файлов в корневом каталоге распакованного дистрибутива. Имена файлов в нем сами по себе способны нести полезную информацию.


Правило 16: Используйте одинаковые формы new и delete

Из книги автора

Правило 16: Используйте одинаковые формы new и delete Что неправильно в следующем фрагменте?std::string *stringArray = new std::string[100];...delete stringArray;На первый взгляд, все в полном порядке – использованию new соответствует применение delete, но кое-что здесь совершенно неверно. Поведение программы


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

Из книги автора

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


Правило 52: Если вы написали оператор new с размещением, напишите и соответствующий оператор delete

Из книги автора

Правило 52: Если вы написали оператор new с размещением, напишите и соответствующий оператор delete Операторы new и delete с размещением встречаются в C++ не слишком часто, поэтому в том, что вы с ними не знакомы, нет ничего страшного. Вспомните (правила 16 и 17), что когда вы пишете такое


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

Из книги автора

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


19.2.4.3. Придерживайтесь стандартной практики именования файлов

Из книги автора

19.2.4.3. Придерживайтесь стандартной практики именования файлов Еще до просмотра README-файла бесстрашный исследователь внимательно изучит имена файлов в корневом каталоге распакованного дистрибутива. Имена файлов в нем сами по себе способны нести полезную информацию.


4.9. Операторы new и delete

Из книги автора

4.9. Операторы new и delete Каждая программа во время работы получает определенное количество памяти, которую можно использовать. Такое выделение памяти под объекты во время выполнения называется динамическим, а сама память выделяется из хипа (heap). (Мы уже касались вопроса о


15.8.1. Операторы new[ ] и delete [ ]

Из книги автора

15.8.1. Операторы new[ ] и delete [ ] Оператор new(), определенный в предыдущем подразделе, вызывается только при выделении памяти для единичного объекта. Так, в данной инструкции вызывается new() класса Screen:// вызывается Screen::operator new()Screen *ps = new Screen( 24, 80 );тогда как ниже вызывается


Выработка соглашений для динамических псевдонимов

Из книги автора

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


Анализ поправок, принятых Госдумой, к закону «Об информации, информационных технологиях и о защите информации» Сергей Голубицкий

Из книги автора

Анализ поправок, принятых Госдумой, к закону «Об информации, информационных технологиях и о защите информации» Сергей Голубицкий Опубликовано 26 июня 2013 21 июня Государственная Дума РФ приняла сразу во втором и третьем чтении Федеральный Закон «О


4.3.3. Создание списка задач, принятых к исполнению

Из книги автора

4.3.3. Создание списка задач, принятых к исполнению Каждый календарь может иметь свой набор задач, принятых к исполнению. Перечень задач отображается на панели, которая раскрывается нажатием на кнопку: Чтобы создать задачу, необходимо:1. Выбрать требуемый календарь на