Как написать стандарт ISO C ++, соответствующий пользовательским операторам new и delete? - PullRequest
60 голосов
/ 25 августа 2011

Как написать стандарт ISO C ++, соответствующий пользовательским new и delete операторам?

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

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

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

(Примечание. Предполагается, что это будет вход в FAQ по C ++ для Stack Overflow . Если вы хотите критиковать идею предоставления FAQ в этой форме, тогда публикация по мета, которая началась все это будет местом для этого. Ответы на этот вопрос отслеживаются в C ++ чате , где идея FAQ возникла в первую очередь, поэтому ваш ответ, скорее всего, будет прочитан теми, кто придумал эту идею.)
Примечание: ответ основан на уроках Скотта Мейерса «Более эффективный C ++» и стандарта ISO C ++.

Ответы [ 4 ]

31 голосов
/ 25 августа 2011

Часть I

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

Реализация пользовательского 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;
}

InВ приведенном выше примере operator new (наиболее вероятно) не сможет выделить место для 100 000 000 целых чисел, и будет вызвана функция outOfMemHandler(), и программа прекратит работу после , выдавшей сообщение об ошибке . * 1106.*

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

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


19 голосов
/ 25 августа 2011

Часть 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 должен попытаться выделить память более одного раза, вызывая функцию новой обработки после каждого сбоя. Здесь предполагается, что функция новой обработки может сделать что-то, чтобы освободить память. Только когда указатель на новую функцию обработки равен 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

16 голосов
/ 25 августа 2011

Часть 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;
}

Как и в этом FAQ, объясняется, что общая причина написания настраиваемого менеджера памяти - оптимизировать выделение для объектов определенного класса, а не дляclass или любой из его производных классов, что в основном означает, что наш новый оператор для базового класса обычно настраивается для объектов размером 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 ++, доктора Бьярна Страуструпа.

11 голосов
/ 25 августа 2011

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

Стандартная библиотека 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 ++ 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;
        }
};
...