Как я должен писать стандартные стандартные и удаленные операторы ISO С++ Standard?

Как мне написать стандартные стандартные пользовательские операторы new и delete ISO С++?

Это продолжение Перегрузка новых и удаление в чрезвычайно освещающем FAQ на С++, Перегрузка оператора, и последующие действия, Зачем заменять операторы по умолчанию и delete по умолчанию?

Раздел 1: Запись стандартно-совместимого оператора new

Раздел 2: Написание стандартно-совместимого оператора delete

<суб > (Примечание. Это должно быть запись в Часто задаваемые вопросы о переполнении стека С++. Если вы хотите критиковать идею предоставления FAQ в этой форме, тогда публикация на мета, которая начала все это, была бы местом для этого. Ответы на этот вопрос отслеживаются в С++ чате, где идея FAQ начиналась в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)
Примечание. Ответ основан на знаниях Скотта Майерса "Эффективный С++" и стандарта ISO С++.

суб >

56
25 авг. '11 в 19:43
источник поделиться
4 ответов

Часть I

В этой статье С++ FAQ объясняется, почему можно перегрузить операторы new и delete для одного собственного класса. В настоящем FAQ часто объясняется, как это делается стандартно.

Реализация пользовательского оператора new

Стандарт С++ (§18.4.1.1) определяет operator new как:

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

Стандарт С++ указывает семантику, которую пользовательские версии этих операторов должны подчиняться в п. 3.7.3 и § 18.4.1.

Подведем итоги.

Требование № 1:. Оно должно динамически выделять не менее size байт памяти и возвращать указатель на выделенную память. Цитата из стандарта С++, раздел 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 - это функция, которая принимает и возвращает a new_handler.

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

Предполагаемый момент для образца кода, чтобы все было ясно:

#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 (скорее всего) не сможет выделить место для 100 000 000 целых чисел, и будет вызвана функция outOfMemHandler(), и программа прервется после выдача сообщения об ошибке.

Важно отметить, что когда operator new не может выполнить запрос памяти, он вызывает функцию new-handler несколько раз, пока не найдет достаточно памяти или новых обработчиков не будет. В приведенном выше примере, если мы не назовем std::abort(), outOfMemHandler() будет вызываться повторно. Поэтому обработчик должен либо гарантировать, что следующее распределение будет успешным, либо зарегистрировать другой обработчик, либо не зарегистрировать обработчик, либо не вернуть (т.е. Завершить программу). Если нового обработчика нет, и выделение не выполняется, оператор выдает исключение.

Продолжение 1


29
25 авг. '11 в 19:43
источник

Связанные вопросы


Похожие вопросы

Часть 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 и set_new_handler, мы можем изменить Требование № 4 соответственно:

Требование №4 (расширенное):
Наш operator new должен попытаться распределить память более одного раза, вызвав функцию new-handling после каждого отказа. Предположение здесь состоит в том, что новая функция обработки может сделать что-то, чтобы высвободить некоторую память. Только тогда, когда указатель на функцию new-handling 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

    }

}

Продолжение 2

18
25 авг. '11 в 19:44
источник

Часть III

... продолжение

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

Кроме того, у нас есть бесконечный цикл, и единственный выход из цикла - это то, что память будет успешно распределена, или для функции new-handling выполнить одну из вещей, которые мы предположили раньше. Если new_handler не делает одну из этих вещей, этот цикл внутри оператора new никогда не завершится.

Оговорка: Обратите внимание, что стандарт (§3.7.4.1.3, цитируемый выше) явно не говорит о том, что перегруженный new оператор должен реализовывать бесконечный цикл, но он просто говорит, что это поведение по умолчанию. Таким образом, эта деталь открыта для интерпретации, но большинство компиляторов (GCC и Microsoft Visual С++) реализовать эту функцию цикла (вы можете скомпилировать примеры кода, предоставленные ранее). Кроме того, поскольку С++ authory, например Скотт Майерс предлагает такой подход, это достаточно разумно.

Специальные сценарии

Рассмотрим следующий сценарий.

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 наследует новый оператор базового класса. Это делает оператор вызова новым в базовом классе для выделения памяти для объекта производного класса. Лучший способ для нашей operator 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. Это связано с тем, что все автономные объекты имеют ненулевой размер в С++, поэтому sizeof(Base) никогда не может быть нулевым, поэтому, если размер равен нулю, запрос будет перенаправлен на ::operator new, и он будет гарантировать, что он будет обрабатывать его в стандартный способ.

Образец цитирования: От создателя самого С++, д-ра Bjarne Stroustrup.

15
25 авг. '11 в 19:45
источник

Реализация пользовательского оператора delete

Библиотека С++ Standard (§18.4.1.1) определяет operator delete как:

void operator delete(void*) throw();

Повторим упражнение по сбору требований для написания нашего пользовательского operator delete:

Требование № 1: Он должен вернуть void, а его первый параметр должен быть void*. Пользовательский delete operator может иметь более одного параметра, но нам нужен только один параметр, чтобы передать указатель, указывающий на выделенную память.

Цитата из стандарта С++:

Раздел §3.7.3.2.2:

"Каждая функция освобождения должна возвращать void, и ее первый параметр должен быть недействительным *. Функция дезадаптации может иметь более одного параметра....."

Требование № 2: оно должно гарантировать безопасное удаление нулевого указателя, переданного в качестве аргумента.

Цитата из С++ Standard: Раздел §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 должен освободить динамическую память, выделенную и назначенную указателю.

Цитата из С++ Standard: Раздел §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;
        }
};
10
25 авг. '11 в 21:15
источник

Посмотрите другие вопросы по меткам или Задайте вопрос