Как C++ освобождава паметта, когато конструктор хвърля изключение и се използва персонализирано ново

Виждам следните конструкции:

  • new X ще освободи паметта, ако X конструкторът хвърли.

  • operator new() може да бъде претоварен.

Каноничната дефиниция на оператор ново претоварване е void *operator new(size_t c, heap h) и съответното operator delete.

Най-често срещаният оператор new overload е placement new, който е void *operator new(void *p) { return p; }

Почти винаги не можете да извикате delete на указателя, даден на разположение new.

Това води до един въпрос: Как се почиства паметта, когато конструкторът X хвърля и се използва претоварен new?


person Joshua    schedule 30.10.2013    source източник
comment
Кое претовари new? Поставянето - ново разнообразие?   -  person John Dibling    schedule 30.10.2013
comment
Трябва внимателно да отбележите, че стандартът C++ (поне C++03) не позволява на програма да претоварва placement-new.   -  person John Dibling    schedule 30.10.2013
comment
@John Dibling: отидете да прочетете new.h   -  person Joshua    schedule 30.10.2013
comment
Не съм сигурен, че разбирам мисълта ти? Този файл е или реализация на C++ Standard Library, или е несъответстващ.   -  person John Dibling    schedule 30.10.2013
comment
Въпросът е, че компилаторът по дефиниция трябва да може да компилира частите от стандартната библиотека, написана на C++, и чрез разширение, ако не се свържете с него, можете да го замените.   -  person Joshua    schedule 30.10.2013
comment
Значи говорите за внедряване на ваша собствена C++ стандартна библиотека?   -  person John Dibling    schedule 30.10.2013


Отговори (5)


Когато конструктор хвърли изключение, се извиква съответстващото изтриване. Деструкторът не се извиква за класа, който е хвърлил, но всички компоненти на класа, чиито конструктори са извикани успешно, ще бъдат извикани и деструкторите им.

person stonemetal    schedule 30.10.2013
comment
Това е погрешно тълкуване на страницата в Уикипедия, която от своя страна има някои неточности. Примерът в страницата на Wikipedia за заменена двойка нов/изтриване е само пример; можете да използвате произволна последователност от допълнителни типове аргументи (различни от void*, който е запазен). Подозирам, че намерението на примера в Уикипедия е тип А да бъде някакъв разпределител. Във всеки случай можете да извикате по подразбиране ::delete(void*, void*), ако желаете, но това няма да направи нищо. - person rici; 30.10.2013

По принцип, ако няма оператор delete, който да съответства на оператора new, тогава нищо не се прави. Нищо не се прави и в случай на ново разположение, тъй като съответният оператор за изтриване на разположение е no-op. Изключението не се отклонява: то продължава своя курс, така че извикващият новия има възможност (и отговорност) за освобождаване на разпределената памет.

Поставянето new се нарича така, защото се използва за поставяне на обекта в паметта, придобита по друг начин; тъй като паметта не е придобита от новия оператор, е малко вероятно тя да може да бъде освободена от оператора за изтриване. На практика въпросът е спорен, тъй като (поне от C++03) не е позволено да се заменя нов оператор за разположение (който има прототип operator new(size_t, void*) или delete (operator delete(void*, void*)). Предоставеният нов оператор за разположение връща втория си аргумент и предоставеният оператор за изтриване на разположение е без операция.

Други new и delete оператори могат да бъдат заменени, глобално или за конкретен клас. Ако се извика персонализиран оператор new и конструкторът хвърли изключение и има съответен оператор delete, тогава този оператор за изтриване ще бъде извикан за почистване, преди изключението да бъде разпространено. Въпреки това не е грешка, ако няма съответстващ оператор delete.

person rici    schedule 30.10.2013

Първо, пример:

#include <cstddef>
#include <iostream>

struct S
{
    S(int i) { if(i > 42) throw "up"; }

    static void* operator new(std::size_t s, int i, double d, char c)
    {
        std::cout << "allocated with arguments: "
                  <<i<<", "<<d<<", "<<c<<std::endl;
        return new char[s];
    }

    static void operator delete(void* p, int i, double d, char c)
    {
        std::cout << "deallocated with arguments: "
                  <<i<<", "<<d<<", "<<c<<std::endl;
        delete[] (char*)p;
    }

    static void operator delete(void* p)
    {
        std::cout << "deallocated w/o arguments"<<std::endl;
        delete[] (char*)p;
    }
};

int main()
{
    auto p0 = new(1, 2.0, '3') S(42);

    S* p1 = nullptr;
    try
    {
        p1 = new(4, 5.0, '6') S(43);
    }catch(const char* msg)
    {
        std::cout << "exception: "<<msg<<std::endl;
    }

    delete p1;
    delete p0;
}

Изход:

allocated with arguments: 1, 2, 3
allocated with arguments: 4, 5, 6
deallocated with arguments: 4, 5, 6
exception: up
deallocated w/o arguments

Каноничната дефиниция на оператор ново претоварване е void *operator new(std::size_t, heap h)

Не виждам как това е канонично, тъй като не е позволено: Добре, сега това е валидна форма за разположение на new :)

[basic.stc.dynamic.allocation]/1

Функцията за разпределяне трябва да бъде функция член на клас или глобална функция; програма е неправилно оформена, ако функция за разпределение е декларирана в обхват на пространство от имена, различен от глобален обхват или декларирана като статична в глобален обхват. Върнатият тип трябва да бъде void*. Първият параметър трябва да има тип std::size_t. Първият параметър не трябва да има свързан аргумент по подразбиране. Стойността на първия параметър се тълкува като искания размер на разпределението.

[акцентът е мой]

Можете да претоварите функцията за разпределение, която да бъде извикана за формуляра за разположение на new, вижте [expr.new] (не е изрично разрешено в [basic.stc.dynamic.allocation] за нешаблонни функции, но също така не е забранено). Разположението, дадено в new(placement), е обобщено тук до списък с изрази. Всеки израз в списъка с изрази за конкретен нов израз се предава като допълнителни аргументи към функцията за разпределение. Ако се извика функцията за освобождаване (напр. защото извиканият ctor хвърля изключение), същите аргументи плюс водещо void* (върната стойност на функцията за разпределение) се предават на функцията за освобождаване.

[expr.new]/18 състояния:

Ако някоя част от инициализацията на обекта, описана по-горе, приключи чрез хвърляне на изключение, за обекта е получено съхранение и може да бъде намерена подходяща функция за освобождаване, функцията за освобождаване се извиква, за да освободи паметта, в която обектът е конструиран, след което изключението продължава да разпространява в контекста на новия израз. Ако не може да се намери недвусмислено съответстваща функция за освобождаване, разпространението на изключението не води до освобождаване на паметта на обекта. [Забележка: Това е подходящо, когато извиканата функция за разпределение не разпределя памет; в противен случай има вероятност да доведе до изтичане на памет. — крайна бележка ]

и /21

Ако нов израз извика функция за освобождаване, той предава стойността, върната от извикването на функцията за разпределение, като първи аргумент от тип void*. Ако се извика функция за освобождаване на разположение, на нея се подават същите допълнителни аргументи, които са били предадени на функцията за разпределяне на разположение, т.е. същите аргументи като тези, посочени със синтаксиса за ново разположение.

и /20

Декларация на функция за освобождаване на разположение съвпада с декларацията на функция за разпределяне на разположение, ако има същия брой параметри и след трансформации на параметри всички типове параметри с изключение на първия са идентични. Всяка функция за освобождаване без разположение съответства на функция за разпределение без разположение. Ако търсенето намери една съвпадаща функция за освобождаване, тази функция ще бъде извикана; в противен случай няма да бъде извикана функция за освобождаване. Ако търсенето открие формата с два параметъра на обичайна функция за освобождаване и тази функция, считана за функция за освобождаване на разположение, би била избрана като съвпадение за функцията за разпределение, програмата е неправилно оформена. [Пример:

struct S {
    // Placement allocation function:
    static void* operator new(std::size_t, std::size_t);
    // Usual (non-placement) deallocation function:
    static void operator delete(void*, std::size_t);
};

S* p = new (0) S; // ill-formed: non-placement deallocation function matches
                  // placement allocation function

краен пример ]

Връщане към [basic.stc.dynamic.deallocation]:

1 Функциите за освобождаване трябва да бъдат функции на клас или глобални функции; програма е неправилно оформена, ако функциите за освобождаване са декларирани в обхват на пространство от имена, различен от глобален обхват или декларирани като статични в глобален обхват.

2 Всяка функция за освобождаване трябва да върне void и нейният първи параметър трябва да бъде void*. Функцията за освобождаване може да има повече от един параметър.

person dyp    schedule 30.10.2013

'разположение ново' не е претоварена версия на new, а един от вариантите на оператора new и също такъв, който не може да бъде претоварен.

Можете да видите списъка с нови оператори тук заедно с описание на как работи претоварването им.

Ако конструктор хвърли изключение при използване на placement new, компилаторът знае какъв нов оператор е използван и извиква placement delete.

person Mihai Stan    schedule 30.10.2013

Когато конструирането на обекта, конструиран като част от new-expression, е неуспешно, ще бъде извикана съответната функция за освобождаване, ако има такава. Например

new X;

ще използва следната двойка функции за разпределяне/освобождаване.

void * operator new(std::size_t);
void operator delete(void *);

По същия начин, за поставяне на нов формуляр

new(&a) X;

ще се използват версиите за разположение на функцията operator new и operator delete.

void * operator new(std::size_t, void *);
void operator delete(void *, void *);

Имайте предвид, че последната функция умишлено не извършва действие.

person avakar    schedule 30.10.2013