Если я пишу операторы new и delete для класса, должен ли я писать все их перегрузки?

на справочной странице C++ перечислены 8 определенный класс перегрузок для глобальные новые операторы. Четыре из них были добавлены для 2017 версии C++.

Class-специфические функции распределения

void* T::operator new  ( std::size_t count );   
void* T::operator new[]( std::size_t count );
void* T::operator new  ( std::size_t count, std::align_val_t al ); // (since C++17)
void* T::operator new[]( std::size_t count, std::align_val_t al ); // (since C++17)

Class-specific функции распределения размещения

void* T::operator new  ( std::size_t count, user-defined-args... );
void* T::operator new[]( std::size_t count, user-defined-args... );
void* T::operator new  ( std::size_t count,
    std::align_val_t al, user-defined-args... ); // (since C++17)
void* T::operator new[]( std::size_t count,
     std::align_val_t al, user-defined-args... ); // (since C++17)

на этом веб-сайте также перечислены 10 определенный класс версии глобальные операторы удаления, из которых 4 были введены для 2017.

Class-specific обычные функции освобождения

void T::operator delete  ( void* ptr );
void T::operator delete[]( void* ptr );
void T::operator delete  ( void* ptr, std::align_val_t al ); // (since C++17)
void T::operator delete[]( void* ptr, std::align_val_t al ); // (since C++17)
void T::operator delete  ( void* ptr, std::size_t sz );
void T::operator delete[]( void* ptr, std::size_t sz );
void T::operator delete  ( void* ptr, std::size_t sz, std::align_val_t al ); // (since C++17)
void T::operator delete[]( void* ptr, std::size_t sz, std::align_val_t al ); // (since C++17)

Class-specific размещение функции освобождения

void T::operator delete  ( void* ptr, args... );
void T::operator delete[]( void* ptr, args... );

если я пишу класс C++ с операторами new и delete, мне нужно перегрузить все из них? Я игнорирую заменяемые глобальные операторы, так как я пишу только операторы класса.

этот другой вопрос предоставляет информацию о написании совместимых с ISO новых и удаленных операторов, но не говорит, Должен ли я перегружать их всех или только некоторых.

ответ этот вопрос о классе конкретных операторов new и delete не говорит ли заменить все или только некоторые из них.

если вы можете предоставить цитаты из стандарта C++ или комментарии экспертов по памяти C++, это поможет.

3 ответов


нет, вам не нужно писать все варианты операторов new и delete для вашего класса.

есть несколько причин, чтобы предпочесть некоторые версии new и delete над другими. Я опишу каждую причину отдельно.

почти всегда предпочитают операторы удаления, которые имеют параметр размера над теми, у кого его нет.

когда я пишу операторы удаления для базового класса, который обеспечивает обработку памяти для других классы, я использую эти версии операторов delete

void T::operator delete  ( void* ptr, std::size_t sz );
void T::operator delete[]( void* ptr, std::size_t sz );
void T::operator delete  ( void* ptr, std::size_t sz, std::align_val_t al ); // (since C++17)
void T::operator delete[]( void* ptr, std::size_t sz, std::align_val_t al ); // (since C++17)

и намеренно пропускать или =delete эти версии.

void T::operator delete  ( void* ptr );
void T::operator delete[]( void* ptr );
void T::operator delete  ( void* ptr, std::align_val_t al ); // (since C++17)
void T::operator delete[]( void* ptr, std::align_val_t al ); // (since C++17)

причина в том, что std::size_t sz параметр сообщает мне размер объекта или размер массива. Я не могу знать размеры объектов производного класса при написании базового класса, поэтому использование параметра size помогает. Некоторые из моих обработчиков памяти разделяют объекты по размеру (проще объединить память, когда все куски имеют одинаковый размер). Я можно использовать параметр size для быстрого выбора пула памяти для поиска, а не для поиска всех из них. Это превращает алгоритм O(n) в действие O(1).

некоторые из моих распределителей памяти используют " цепную модель "вместо" блочной модели", и параметр size также помогает удалить ее. (Я называю распределитель памяти "блочной моделью", если он предварительно выделяет огромный кусок, а затем разбивает его на отдельные блоки, такие как массив. Я называю обработчик памяти "цепной моделью", если каждый chunk указывает на предыдущие и следующие куски, такие как связанный список или цепочка.) Поэтому, когда кто-то удаляет фрагмент из цепочки фрагментов памяти, я хочу, чтобы оператор delete знал, что удаляемый фрагмент имеет правильный размер. Я могу поместить утверждение в операцию удаления, которая утверждает (size == address следующего фрагмента-адрес этого фрагмента).

при необходимости предпочтите операторы new и delete с параметром выравнивания.

теперь, когда C++17 предоставляет параметр выравнивания для новых операторов, используйте их, если они вам нужны. Если вам нужна производительность, выровнять объекты на 4, 8 или 16 байт, не так! Это делает программу немного быстрее.

Итак, предположим, у вас есть распределитель памяти с выравниванием. Он знает, что некоторые объекты лучше всего хранить на 4-байтовых границах, потому что эти объекты малы, и вы можете сжать больше в память, если вы используете 4-байтовые границы. Он также знает, что некоторые объекты лучше выровнены по 8 байтов потому что эти объекты часто используются.

обработчик памяти будет знать это, если он предоставляет правильные новые операторы и производные классы предоставляют правильные значения для выравниваний.

стандарт 2017 C++ говорит:

при выделении объектов и массивов объектов, выравнивание которых превышает STDCPP_DEFAULT_NEW_ALIGNMENT, разрешение перегрузки выполняется два раза: во-первых, для выравнивание-зависимые функции подписи, потом выравнивание-неосознанные сигнатуры функций. Это означает, что если класс с расширенным выравниванием имеет функцию распределения, не учитывающую выравнивание, то будет вызываться функция, а не глобальная функция распределения, учитывающая выравнивание. Это намеренно: ожидается, что член класса лучше всего знает, как обращаться с этим классом.

это означает, что компилятор будет проверять операторы new и delete с выравниванием параметр, а затем проверьте наличие операторов без параметра выравнивания.

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

void* T::operator new  ( std::size_t count, std::align_val_t al ); // (since C++17)
void* T::operator new[]( std::size_t count, std::align_val_t al ); // (since C++17)
void* T::operator new  ( std::size_t count,
    std::align_val_t al, user-defined-args... ); // (since C++17)
void* T::operator new[]( std::size_t count,
     std::align_val_t al, user-defined-args... ); // (since C++17)

вы можете заставить код предоставить параметр выравнивания, если вы предоставите вышеуказанные новые операторы и опустите или =delete эти перегрузки.

void* T::operator new  ( std::size_t count );
void* T::operator new[]( std::size_t count );

void* T::operator new  ( std::size_t count, user-defined-args... );
void* T::operator new[]( std::size_t count, user-defined-args... );

использовать класс конкретного размещения-new операторы для предоставления подсказок.

предположим, вы написали класс, который выделяет несколько членов данных, и вы хотите, чтобы все эти члены данных были расположены на одной странице памяти. Если данные распределены по нескольким страницам памяти, ЦП должен будет загрузить разные страницы памяти в кэш L1 или L2, чтобы вы могли получить доступ к данным-членам для объекта. Если обработчик памяти может разместить все элементы данных объекта на одной странице, программа будет работать быстрее потому что CPU не нужно будет загружать несколько страниц в кэш.

это новые операторы размещения класса.

void* T::operator new  ( std::size_t count, user-defined-args... );
void* T::operator new[]( std::size_t count, user-defined-args... );
void* T::operator new  ( std::size_t count,
    std::align_val_t al, user-defined-args... ); // (since C++17)
void* T::operator new[]( std::size_t count,
     std::align_val_t al, user-defined-args... ); // (since C++17)

перегрузите их, чтобы выглядеть так, предоставив параметр подсказки.

void* T::operator new  ( std::size_t count, void* hint );
void* T::operator new[]( std::size_t count, void* hint );
void* T::operator new  ( std::size_t count, std::align_val_t al, void* hint ); // (since C++17)
void* T::operator new[]( std::size_t count, std::align_val_t al, void* hint ); // (since C++17)

параметр hint указывает обработчику памяти попытаться разместить объект не в месте этого адреса подсказки, а на той же странице, что и адрес подсказки.

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

class Foo : public MemoryHandler
{
public:
    Foo();
    ...
private:
    Blah * b_;
    Wham * f_;
};

Foo::Foo() : b_( nullptr ), f_( nullptr )
{
    // This should put data members on the same memory page as this Foo object.
    b_ = new ( this ) Blah;
    f_ = new ( this ) Wham;
}

вам нужно только перегрузить версии new и delete что вы используете. Согласно примеру в [class.free], определяя operator new функция в классе скроет все глобальные operator new функции. Это то же самое, что определение метода с тем же именем, что и функция базового класса, или глобальная функция скрывает базовую или глобальную версии.

отметим, что operator new и operator new[] разные имена, поэтому перегрузка operator new сам по себе не скроет глобальный operator new[] функции.


если я пишу класс C++ с операторами new и delete, мне нужно перегрузить все это?

нет, вам не нужно перегружать их всех. Как минимум, вам нужно перегрузить операторов, которые вам нужно настроить.

Я думаю, мы можем предположить, что вы делаете что-то конкретное в перегруженных операторах, иначе вы бы не нуждались в них в любом случае.

вопрос становится более должен ли я перегрузить все эти?

да, вероятно, вы должны. Было бы удивительно, если бы код делал совершенно разные вещи в зависимости от формы new или delete используется в код, например

auto* obj1 = new Obj{};
// vs
auto* obj2 = new Obj[5];

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

обратная сторона этого заключается в том, что если другие формы неприменимы, то предпочтение удаления (= delete) полностью перегрузок.

операторы C++ входите в "наборы", арифметику, вставки и извлечения потока, реляционные и т. д. Это обычная практика, когда один из операторов в наборе перегружен, другие также.

это не всегда применяется, но обычно делает. Е. Г. операции конкатенации часто имеют operator+ и operator+=, а не operator- и operator-=