static void operator delete(void*, std::size_t);
};
Вы вводите простой протокол для выделения и освобождения памяти.
• Вызывающий код может выделять объекты типа T
либо при помощи распределителя по умолчанию (используя вызов new T
), либо при помощи пользовательского распределителя (вызов new(allос) T
, где allос
— объект типа CustomAllocator
).
• Единственный оператор delete
, который может быть использован вызывающим кодом — оператор по умолчанию operator delete(size_t)
, так что, конечно, вы должны реализовать его так, чтобы он корректно освобождал память, выделенную любым способом.
Пока все в порядке.
Однако компилятор может скрыто вызвать другую перегрузку оператора delete
, а именно T::operator delete(size_t, CustomAllocator&)
. Это связано с тем, что инструкция
T* р = new(alloc) T;
на самом деле разворачивается в нечто наподобие
// Сгенерированный компилятором код для
// инструкции T* p = new(alloc)T;
//
void* __compilerTemp = T::operator new(sizeof(T), alloc);
T* p;
try {
p = new (__compilerTemp) T; // Создание объекта T по
// адресу __compilerTemp
} catch(...) { // Сбой в конструкторе...
T::operator delete(__compilerTemp, sizeof(T), alloc);
throw;
}
Итак, компилятор автоматически вставляет код вызова соответствующего оператора T::operator delete
для перегруженного оператора T::operator new
, что совершенно логично, если выделение памяти прошло успешно, но произошел сбой в конструкторе. 'Соответствующая' сигнатура оператора delete
имеет вид void operator delete(void*, параметры_оператора_new)
.
Теперь перейдем к самому интересному. Стандарт С++ ([C++03] §5.3.4(17)) гласит, что приведенный выше код будет генерироваться тогда и только тогда, когда реально существует соответствующая перегрузка оператора delete
. В противном случае код вообще не будет вызывать никакого оператора delete
при сбое в конструкторе. Другими словами, при сбоях в конструкторе мы получим утечку памяти. Из шести проверенных нами распространенных компиляторов только два выводили предупреждение в такой ситуации. Вот почему каждый перегруженный оператор void* operator new(parms)
должен сопровождаться соответствующей перегрузкой void operator delete(void*, parms)
.
Размещающий оператор new
void* T::operator new(size_t, void* p) { return p; }
не требует наличия соответствующего оператора delete
, поскольку реального выделения памяти при этом не происходит. Все протестированные нами компиляторы не выдавали никаких предупреждений по поводу отсутствия оператора void T::operator delete(void*, size_t, void*)
.
46. При наличии пользовательского new
следует предоставлять все стандартные типы этого оператора
Если класс определяет любую перегрузку оператора new
, он должен перегрузить все три стандартных типа этого оператора — обычный new
, размещающий и не генерирующий исключений. Если этого не сделать, то эти операторы окажутся скрытыми и недоступными пользователям вашего класса.
Обычно пользовательские операторы new
и delete
нужны очень редко, но если они все же оказываются необходимы, то вряд ли вы захотите, чтобы они скрывали встроенные сигнатуры.
В С++, после того как вы определите имя в области видимости (например, в области видимости класса), все такие же имена в охватывающих областях видимости окажутся скрыты (например, в базовых классах или охватывающих пространствах имен), так что перегрузка никогда не работает через границы областей видимости. Когда речь идет об имени оператора new
, необходимо быть особенно осторожным и внимательным, чтобы не усложнять жизнь себе и пользователям вашего класса.
Пусть вы определили следующий оператор new
, специфичный для класса:
class С {
// ...
// Скрывает три стандартных вида оператора new
static void* operator new(size_t, MemoryPool&);
};
Теперь, если кто-то попытается написать выражение с обычным стандартным new
С, компилятор сообщит о том, что он не в состоянии найти обычный старый оператор new
. Объявление перегрузки C::operator new
с параметром типа MemoryPool
скрывает все остальные перегрузки, включая знакомые встроенные глобальные версии, которые все мы знаем и любим:
void* operator new(std::size_t); // Обычный
void* operator new(std::size_t,
std::nothrow_t) throw(); // He генерирующий исключений
void* operator new(std::size_t,
void*); // Размещающий
В качестве другого варианта событий предположим, что ваш класс предоставляет некоторую специфичную для данного класса версию оператора new
— одну из трех. В таком случае это объявление также скроет остальные две версии:
class С {
// ...
// Скрывает две другие стандартные версии оператора new
static void* operator new(size_t, void*);
};
Предпочтительно, чтобы у класса С в его область видимости были явно внесены все три стандартные версии оператора new
. Обычно все они должны иметь одну и ту же видимость. (Видимость для отдельных версий может быть сделана закрытой, если вы хотите явно запретить один из вариантов оператора new
, однако цель данной рекомендации — напомнить, чтобы вы не скрыли эти