Как написать стандарт ISO C++, соответствующий пользовательским операторам new и delete?
Как написать стандарт ISO C++ new
а также delete
операторы?
Это продолжение перегрузки новых и удаления в чрезвычайно освещенном разделе часто задаваемых вопросов по С ++, перегрузке операторов и ее последующей деятельности. Почему следует заменить операторы new и delete по умолчанию?
Раздел 1: Написание стандартного конформанта new
оператор
- Часть 1: Понимание требований для написания пользовательских
new
оператор - Часть 2: Понимание
new_handler
требования - Часть 3: Понимание конкретных требований сценария
Раздел 2: Написание стандартного конформанта delete
оператор
(Примечание. Предполагается, что это будет вход в FAQ по C++ в Stack Overflow. Если вы хотите критиковать идею предоставления FAQ в этой форме, то публикация в meta, с которой все это началось, будет подходящим местом для этого. этот вопрос отслеживается в чате C++, где идея FAQ возникла в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)
Примечание. Ответ основан на уроках Скотта Мейерса "Более эффективный C++" и стандарта ISO C++.
4 ответа
Часть I
В этой статье C++ FAQ объясняется, почему можно перегружать new
а также delete
операторы для своего класса. Настоящий FAQ пытается объяснить, как это делается стандартным образом.
Реализация кастома new
оператор
Стандарт C++ (§18.4.1.1) определяет operator new
как:
void* operator new (std::size_t size) throw (std::bad_alloc);
Стандарт C++ определяет семантику, которой должны подчиняться пользовательские версии этих операторов в §3.7.3 и §18.4.1
Давайте обобщим требования.
Требование № 1: он должен динамически выделять как минимум size
байты памяти и возвращают указатель на выделенную память. Цитата из стандарта C++, раздел 3.7.4.1.3:
Функция выделения пытается выделить запрошенный объем памяти. Если он успешен, он должен вернуть адрес начала блока памяти, длина которого в байтах должна быть, по крайней мере, такой же, как запрашиваемый размер...
Стандарт далее налагает:
... Возвращаемый указатель должен быть соответствующим образом выровнен, чтобы его можно было преобразовать в указатель любого завершенного типа объекта и затем использовать для доступа к объекту или массиву в выделенном хранилище (до тех пор, пока хранилище не будет явно освобождено путем вызова соответствующего функция освобождения). Даже если размер запрошенного пространства равен нулю, запрос может завершиться ошибкой. Если запрос выполнен успешно, возвращаемое значение должно быть ненулевым значением указателя (4.10) p0, отличным от любого ранее возвращенного значения p1, если только это значение p1 не было впоследствии передано оператору
delete
,
Это дает нам дополнительные важные требования:
Требование № 2: Используемая нами функция выделения памяти (обычно malloc()
или некоторый другой пользовательский распределитель) должен возвращать соответствующим образом выровненный указатель на выделенную память, который может быть преобразован в указатель полного типа объекта и использован для доступа к объекту.
Требование № 3: наш пользовательский оператор new
должен возвращать допустимый указатель, даже когда запрашивается ноль байтов.
Одно из очевидных требований, которые можно вывести из new
прототип это:
Требование № 4: Если new
не может выделить динамическую память запрошенного размера, тогда он должен выдать исключение типа std::bad_alloc
,
Но! Это нечто большее, чем то, что бросается в глаза: если вы посмотрите на new
Документация оператора (цитата из стандарта следует далее ниже) гласит:
Если set_new_handler был использован для определения функции new_handler, это
new_handler
функция вызывается стандартным определением по умолчаниюoperator new
если он не может выделить запрошенное хранилище самостоятельно.
Чтобы понять, как наш обычай new
Необходимо поддержать это требование, мы должны понимать:
Что new_handler
а также set_new_handler
?
new_handler
является typedef для указателя на функцию, которая ничего не принимает и не возвращает, и set_new_handler
это функция, которая принимает и возвращает new_handler
,
set_new_handler
Параметр является указателем на функцию, которую должен вызывать оператор new, если он не может выделить запрошенную память. Его возвращаемое значение - указатель на ранее зарегистрированную функцию-обработчик, или нуль, если предыдущего обработчика не было.
Удачный момент для примера кода, чтобы прояснить ситуацию:
#include <iostream>
#include <cstdlib>
// function to call if operator new can't allocate enough memory or error arises
void outOfMemHandler()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
//set the new_handler
std::set_new_handler(outOfMemHandler);
//Request huge memory size, that will cause ::operator new to fail
int *pBigDataArray = new int[100000000L];
return 0;
}
В приведенном выше примере operator new
(скорее всего) не сможет выделить место для 100000000 целых чисел, а функция outOfMemHandler()
будет вызван, и программа будет прервана после выдачи сообщения об ошибке.
Здесь важно отметить, что когда operator new
не может выполнить запрос памяти, он вызывает new-handler
функционировать несколько раз, пока он не найдет достаточно памяти или больше не будет новых обработчиков. В приведенном выше примере, если мы не называем std::abort()
, outOfMemHandler()
будет вызываться неоднократно. Следовательно, обработчик должен либо гарантировать, что следующее выделение выполнено успешно, либо зарегистрировать другой обработчик, либо не регистрировать обработчик, или не возвращать (то есть завершать программу). Если нового обработчика нет и распределение завершается неудачно, оператор сгенерирует исключение.
Часть II
Учитывая поведение operator new
из примера, хорошо продуманный new_handler
должен сделать одно из следующего:
Сделать больше памяти доступным: это может позволить следующей попытке выделения памяти внутри цикла оператора new быть успешной. Одним из способов реализации этого является выделение большого блока памяти при запуске программы, а затем освобождение его для использования в программе при первом вызове нового обработчика.
Установите другой новый обработчик: если текущий новый обработчик не может сделать больше доступной памяти, и если есть другой новый обработчик, который может, то текущий новый обработчик может установить другой новый обработчик на его место (позвонив set_new_handler
). В следующий раз, когда оператор new вызывает функцию new-handler, он получит самую последнюю установленную функцию.
(Разновидностью этой темы является то, что новый обработчик изменяет свое собственное поведение, поэтому в следующий раз, когда он вызывается, он делает что-то другое. Один из способов добиться этого - заставить новый обработчик изменять статические, специфичные для пространства имен или глобальные данные, которые влияют на поведение нового обработчика.)
Удалите новый обработчик: это делается путем передачи нулевого указателя на set_new_handler
, Без установленного нового обработчика, operator new
сгенерирует исключение ((конвертируется в) std::bad_alloc
) когда выделение памяти неудачно.
Бросить исключение, конвертируемое в std::bad_alloc
, Такие исключения не будут пойманы operator new
, но будет распространяться на сайт, инициирующий запрос памяти.
Не вернуть: по телефону abort
или же exit
,
Для реализации специфичного для класса new_handler
мы должны предоставить классу свои собственные версии set_new_handler
а также operator new
, Класс set_new_handler
позволяет клиентам указывать новый обработчик для класса (точно так же, как стандартный set_new_handler
позволяет клиентам указывать глобальный новый обработчик). Класс operator new
гарантирует, что специфичный для класса new-handler используется вместо глобального new-handler, когда выделяется память для объектов класса.
Теперь, когда мы понимаем new_handler
& set_new_handler
лучше мы можем изменить Требование № 4 соответственно:
Требование № 4 (улучшено):
наш operator new
следует попытаться выделить память более одного раза, вызывая функцию новой обработки после каждого сбоя. Здесь предполагается, что функция новой обработки может что-то сделать, чтобы освободить память. Только когда указатель на новую функцию обработки null
делает operator new
бросить исключение.
Как и было обещано, цитата из Стандарта:
Раздел 3.7.4.1.3:
Функция выделения, которая не выделяет хранилище, может вызвать установленную в данный момент
new_handler
(18.4.2.2
), если есть. [Примечание: предоставляемая программой функция выделения может получить адрес установленной в данный моментnew_handler
с использованиемset_new_handler
функция (18.4.2.3
).] Если функция выделения объявлена с пустой спецификацией исключения (15.4
),throw()
, не может выделить память, он должен возвращать нулевой указатель. Любая другая функция выделения, которая не может выделить хранилище, должна указывать на сбой только путем генерирования исключения классаstd::bad_alloc
(18.4.2.1
) или класс, полученный изstd::bad_alloc
,
Вооружившись требованиями № 4, давайте попробуем псевдокод для нашего new operator
:
void * operator new(std::size_t size) throw(std::bad_alloc)
{
// custom operator new might take additional params(3.7.3.1.1)
using namespace std;
if (size == 0) // handle 0-byte requests
{
size = 1; // by treating them as
} // 1-byte requests
while (true)
{
//attempt to allocate size bytes;
//if (the allocation was successful)
//return (a pointer to the memory);
//allocation was unsuccessful; find out what the current new-handling function is (see below)
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler) //If new_hander is registered call it
(*globalHandler)();
else
throw std::bad_alloc(); //No handler is registered throw an exception
}
}
Часть III
Обратите внимание, что мы не можем получить указатель на новую функцию обработчика напрямую, мы должны вызвать set_new_handler
чтобы узнать, что это такое. Это грубо, но эффективно, по крайней мере, для однопоточного кода. В многопоточной среде, вероятно, потребуется какая-то блокировка для безопасного манипулирования (глобальными) структурами данных, стоящими за новой функцией обработки. (Дополнительные цитаты / подробности приветствуются на этом.)
Кроме того, у нас есть бесконечный цикл, и единственный выход из цикла состоит в том, чтобы память была успешно распределена, или для функции новой обработки выполнялась одна из вещей, которые мы вывели ранее. Если только new_handler
делает одну из этих вещей, этот цикл внутри new
Оператор никогда не прекратит работу.
Предостережение: обратите внимание, что стандарт (§3.7.4.1.3
цитируется выше) явно не говорит о том, что перегружен new
оператор должен реализовать бесконечный цикл, но он просто говорит, что такое поведение по умолчанию. Так что эта деталь открыта для интерпретации, но большинство компиляторов ( GCC и Microsoft Visual C++) реализуют эту функциональность цикла (вы можете скомпилировать примеры кода, предоставленные ранее). Кроме того, поскольку автор C++, такой как Скотт Мейерс, предлагает такой подход, он достаточно разумен.
Особые сценарии
Давайте рассмотрим следующий сценарий.
class Base
{
public:
static void * operator new(std::size_t size) throw(std::bad_alloc);
};
class Derived: public Base
{
//Derived doesn't declare operator new
};
int main()
{
// This calls Base::operator new!
Derived *p = new Derived;
return 0;
}
Как объясняется в этом разделе часто задаваемых вопросов, общая причина написания собственного менеджера памяти состоит в том, чтобы оптимизировать выделение для объектов определенного класса, а не для класса или любого из его производных классов, что в основном означает, что наш оператор, новый для базового класса, обычно настроенный на объекты размера sizeof(Base)
- Ничего большего и ничего меньшего.
В приведенном выше примере из-за наследования производного класса Derived
наследует новый оператор базового класса. Это делает возможным вызов оператора new в базовом классе для выделения памяти для объекта производного класса. Лучший способ для нашего operator new
справиться с этой ситуацией - значит переадресовать такие вызовы, запрашивающие "неправильный" объем памяти, для стандартного оператора new, например:
void * Base::operator new(std::size_t size) throw(std::bad_alloc)
{
if (size != sizeof(Base)) // If size is "wrong,", that is, != sizeof Base class
{
return ::operator new(size); // Let std::new handle this request
}
else
{
//Our implementation
}
}
Обратите внимание, что проверка размера также соответствует нашему требованию № 3. Это потому, что все автономные объекты имеют ненулевой размер в C++, поэтому sizeof(Base)
никогда не может быть нулем, поэтому, если размер равен нулю, запрос будет перенаправлен на ::operator new
и гарантируется, что он будет обрабатывать его стандартным образом.
Образец цитирования: От самого создателя C++, доктора Бьярне Страуструпа.
Реализация пользовательского оператора удаления
Стандарт C++ (§18.4.1.1
) библиотека определяет operator delete
как:
void operator delete(void*) throw();
Давайте повторим упражнение по сбору требований для написания нашего обычая. operator delete
:
Требование № 1: должно вернуться void
и его первый параметр должен быть void*
, Обычай delete operator
также может иметь более одного параметра, но нам просто нужен один параметр для передачи указателя, указывающего на выделенную память.
Цитирование из стандарта C++:
Раздел §3.7.3.2.2:
"Каждая функция освобождения должна возвращать void, а ее первый параметр должен быть пустым *. Функция освобождения может иметь более одного параметра....."
Требование № 2: Это должно гарантировать, что безопасно удалить нулевой указатель, переданный в качестве аргумента.
Цитирование из стандарта C++:раздел §3.7.3.2.3:
Значение первого аргумента, предоставленного одной из функций освобождения, предоставляемых в стандартной библиотеке, может быть значением нулевого указателя; если это так, вызов функции освобождения не имеет никакого эффекта. В противном случае значение предоставляется
operator delete(void*)
в стандартной библиотеке должно быть одно из значений, возвращаемых предыдущим вызовомoperator new(size_t)
или жеoperator new(size_t, const std::nothrow_t&)
в стандартной библиотеке, а значение предоставляетсяoperator delete[](void*)
в стандартной библиотеке должно быть одно из значений, возвращаемых предыдущим вызовомoperator new[](size_t)
или жеoperator new[](size_t, const std::nothrow_t&)
в стандартной библиотеке.
Требование № 3: если переданный указатель не null
тогда delete operator
должен освободить динамическую память, выделенную и назначенную указателю.
Цитирование из стандарта C++:раздел §3.7.3.2.4:
Если аргумент, данный функции освобождения в стандартной библиотеке, является указателем, который не является нулевым значением указателя (4.10), функция освобождения должна освободить память, на которую ссылается указатель, делая недействительными все указатели, ссылающиеся на любую часть выделенное хранилище.
Требование № 4: Кроме того, поскольку наш оператор, специфичный для класса, новый перенаправляет запросы "неправильного" размера в ::operator new
Мы ДОЛЖНЫ пересылать "неправильные" запросы на удаление ::operator delete
,
Таким образом, на основе требований, которые мы суммировали выше, приведен стандартный соответствующий псевдокод для пользовательского delete operator
:
class Base
{
public:
//Same as before
static void * operator new(std::size_t size) throw(std::bad_alloc);
//delete declaration
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; // No-Op is null pointer
}
if (size != sizeof(Base))
{
// if size is "wrong,"
::operator delete(rawMemory); //Delegate to std::delete
return;
}
//If we reach here means we have correct sized pointer for deallocation
//deallocate the memory pointed to by rawMemory;
return;
}
};