Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Шрифт:
Поддержка TMP растет. Вероятно, в следующей версии C++ будет реализована явная поддержка этой технологии, в TR1 это уже декларировано (см. правило 54). Начали появляться книги, посвященные этой теме, а информация о TMP в Internet становится все богаче. Видимо, TMP никогда не станет главным направлением развития, но для некоторых программистов (особенно разработчиков библиотек) она почти наверняка займет важное место.
• Метапрограммирование шаблонов позволяет перенести часть работы со стадии исполнения на стадию компиляции. За счет этого можно раньше обнаружить ошибки и повысить производительность программ.
•
Глава 8
Настройка new и delete
В наши дни, когда вычислительные среды снабжены встроенной поддержкой «сборки мусора» (как, например, Java и. NET), ручной подход C++ к управлению памятью может показаться несколько устаревшим. Однако многие разработчики, создающие требовательные к ресурсам прикладные программы, выбирают C++ именно потому, что он позволяет управлять памятью вручную. Такие разработчики изучают, как используется память в их программах, и разрабатывают собственные процедуры распределения и освобождения памяти с целью достичь максимально возможной производительности (с точки зрения как времени, так и потребления памяти) своих систем.
Для этого нужно понимать, как организованы процедуры управления памятью в C++. Именно этой теме и посвящена настоящая глава. Два основных компонента здесь – это процедура выделения и освобождения памяти (операторы new и delete), а вспомогательная роль отводится обработчику new – функции, которая вызывается, когда new не может удовлетворить запрос на выделение памяти.
С управлением памятью в многопоточной среде связаны дополнительные сложности, не возникающие в однопоточных системах, поскольку «куча» – это модифицируемый глобальный ресурс, доступ к которому должен быть синхронизирован. Во многих правилах из настоящей главы идет речь об использовании модифицируемых статических данных. Эта тема всегда настораживает программистов, разрабатывающих многопоточные программы. Без правильной синхронизации, отсутствия взаимных блокировок в алгоритмах и тщательного проектирования с целью предотвращения одновременного доступа, обращения к процедурам работы с памятью могут легко привести к повреждению структур данных, управляющих кучей. Вместо того чтобы постоянно напоминать вам об этой опасности, я говорю об этом только здесь и предполагаю, что вы будете помнить об этом при чтении остальной части главы.
Следует помнить и о том, что операторы new и delete применимы только к выделению и освобождению одиночных объектов. Память для массивов выделяет operator new[] и освобождает operator delete[] (в обоих случаях «[]» является частью имен функций). Если явно не оговорено противное, то все сказанное об операторах new и delete касается также new[] и delete[].
И наконец, отмечу, что в случае STL-контейнеров выделением памяти из кучи управляют объекты-распределители, ассоциированные с самими контейнерами, а не напрямую new и delete. В этой главе ничего не говорится о распределителях памяти в STL.
Правило 49: Разберитесь в поведении обработчика new
Когда оператор new не может удовлетворить запрос на выделение памяти, он возбуждает исключение. Когда-то он возвращал нулевой указатель, и некоторые старые компиляторы все еще так и поступают. Вы можете столкнуться с таким устаревшим поведением, но я отложу его обсуждение до конца правила.
Прежде чем возбудить исключение в ответ на невозможность удовлетворить запрос на выделение памяти, оператор new вызывает определенную пользователем функцию, называемую обработчиком new (new-handler). (На самом деле это не совсем так. Реальное поведение new
Как видите, 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 не может выделить память для размещения 100 000 000 целых чисел, будет вызвана функция outOfMem, и программа завершится, выдав сообщение об ошибке. (Кстати, подумайте, что случится, если память должна быть динамически выделена во время вывода сообщения об ошибке в cerr…)
Когда operator new не может удовлетворить запрос в памяти, он будет вызывать обработчик new до тех пор, пока он не сумеет найти достаточно памяти. Код, приводящий к повторным вызовам, показан в правиле 51, но и такого высокоуровневого описания достаточно, чтобы сделать вывод о том, что правильно спроектированная функция-обработчик new должна делать что-то одно из следующего списка:
• Найти дополнительную память. В результате следующая попытка выделить память внутри operator new может завершиться успешно. Один из способов реализовать такую стратегию – выделить большой блок памяти в начале работы программы, в затем освободить его при первом вызове обработчика new.
• Установить другой обработчик new. Если текущий обработчик не может выделить память, то, возможно, ему известен какой-то другой, который сможет это сделать. Если так, то текущий обработчик может установить вместо себя другой (вызвав set_new_handler). В следующий раз, когда operator new обратится к обработчику, будет вызван последний установленный. (В качестве альтернативы обработчик может изменить собственное поведение, чтобы при следующем вызове сделать что-то другое. Добиться этого можно, например, путем модификации некоторых статических, определенных в пространстве имен или глобальных данных, влияющих на его поведение.)