понедельник, мая 04, 2009

delete this

Конструкция delete this вполне себе легальна, однако может привести к удивительно неприятным последствиям. Вот здесь написано, что нужно проверить при ее применении: Is it legal (and moral) for a member function to say delete this?. Особенно интересен предпоследний пункт - надо быть уверенным, что после удаления с объектом никто работать не будет.

Вот так вы напишете вряд ли

class CBase
{
int m_i;
public:
...
void MyFunction();
};

void CBase::MyFunction()
{
delete this;
m_i = 5;
}


Однако вот так, очень может быть

void CBase::MyFunction()
{
innocentLookingFunction();
m_i = 5;
}


Т.е. возможно, что строки delete this нет, но innocentLookingFunction() таки ведет к удалению объекта через лихо закрученные вызовы.

Что будет дальше? Ничего хорошего. Access violation, порча чужой памяти, как повезет.

Еще одна известная особенность языка C++ может усугубить проблемы, возникающие при вызове delete this. После удаления объекта можно продолжить с ним работать, вызывать те его функции, что не работают с данными и не подозревать о том, что он вообще удален.

Пример:

class CBase
{
int m_i;
public:
CBase() : m_i (0){}
void printBase() { cout<<"CBase"<<endl; }
};

int main()
{
CBase* b = NULL;
b->printBase();
return 0;
}


Экземпляр объекта CBase никогда даже и не создавался, но код будет шикарно работать годами до тех пор, пока вы не обратитесь к данным класса как-нибудь так:

void printBase() { m_i++; cout<<"CBase"<<endl; }

30 комментариев:

  1. ИМХО конструкция ущербна - слишком небезопасно, а выгода может не стоить того, всегда лучше сделать рефакторинг и переделать архитектуру. Пока за всю мою долгую практику не понадобилось, удавалось избегать и моделировать без этого :)
    Даже стало интересно, в какой идее/модели может понадобится такое? Были ли у вас какие-то примеры из практики?

    ОтветитьУдалить
  2. А где можно про последний пример почитать поподробней? Я так понимаю компилятор работает с функцией как если бы она была static?

    ОтветитьУдалить
  3. Анонимный4/5/09 21:40

    Поскольку в данном случае механизм вызова виртуальных функций не задействован, компилятор просто генерирует статический вызов с неявным параметром -- адресом объекта. Который, опять такиЮ в данном случае не используется -- и все работает. До поры до времени... ;)

    ОтветитьУдалить
  4. Практическая ценность трюка с delete this в создании объектов со счётчиками экземпляров внутри. Других примеров что-то в голову не приходит. Да и этот можно реализовать без таких кулинарных изысков.

    С последним примеров всё довольно просто. Статические функции имеют заявленный список аргументов и не могут обращаться к нестатическим данным класса. Все остальные методы в качестве ещё одного неявного аргумента используют указатель на данные класса, который имеет имя this. И через него и происходит обращение к данным. То есть обращение к m_i на самом деле формируется как this->m_i, поэтому когда b = NULL, то и получим обращение к нулевому указателю. А пока мы не обращаемся к данным класса, то просто вхолостую передаём нулевой указатель которым не пользуемся. А вот с виртуальными функциями такой фокус уже не пройдёт.

    P.S. из "изгибов" программирования

    int *a = NULL;
    int &b = *a; // no error
    int c = *a; // access violation
    b = 123; // access violation

    таким способом можно отложить ошибку, если в аргумент T &v передавать *p, где p - невалидный указатель. В момент получения адреса объекта к нему нет реального обращения, что отложит появление ошибки и затруднит отладку, если такой код использовать.

    ОтветитьУдалить
  5. Анонимный4/5/09 22:25

    ИМХО нужно использовать SharedPointer'ы. И для STL хорошо- он и не перегружает копированием структур и классов- и сам следит за кол-вом ссылок. Надо будет- сам грохнется. А пока SharedPointer в скопе, то кол-во ссылок на объект ненулевое.

    ОтветитьУдалить
  6. Опасно? Да. Но, например, для класса-потока это один из неплохих методов подчистить память, неприбегая к монструозным глобальным списочным структурам. Поток по сути своей независим и в него лазают потенциально реже, так что "delete this" по завершению функции-executor'а потока вполне применимо. Запустили поток и забыли про него. Она за собой уберет.

    Это как goto - в целом ни-ни, но если голова на плечах, то можно.

    ОтветитьУдалить
  7. Анонимный4/5/09 23:49

    Когда объект удаляется, в debug build занимаемая им память должна специально портиться.

    У каждого С++ объекта должен быть ровно один владелец. Если это недостижимо (например сложный объект реализован в DLL и клиент произвольный код) то объект должен быть COM-объектом.

    Если эти 2 условия выполнены, таких проблем не возникнет.

    ОтветитьУдалить
  8. Анонимный5/5/09 00:02

    Александр, для потока есть способ проще: размещать объекты в потоке на стеке, для больших данных использовать размещённые на стеке коллекции, освобождающие память в деструкторе.

    В мире win32 кстати, шоб поток за собой всё убирал, нужно сразу после успешного вызова CreateThread вызвать CloseHandle - довольно как бы неочевидная конструкция :-)

    ОтветитьУдалить
  9. 2reperio:ИМХО конструкция ущербна - слишком небезопасно, а выгода может не стоить того,

    Золотые слова.

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

    Даже стало интересно, в какой идее/модели может понадобится такое? Были ли у вас какие-то примеры из практики?
    Были. Был объект со счетчиком экземпляров, который должен был быть удален, когда экземпляры заканчивались (тут уже упомянули такой пример).

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

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

    2Ilya Kulakov:А где можно про последний пример почитать поподробней? Я так понимаю компилятор работает с функцией как если бы она была static?
    Тут уже успели развернуто ответить.

    2soonts:Когда объект удаляется, в debug build занимаемая им память должна специально портиться.<skip>

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

    ОтветитьУдалить
  10. Анонимный5/5/09 00:51

    На мой взгляд глупая конструкция. Хотя довольно интересно над этим поразмышлять ... например если написать в каком-нибудь методе класса "delete this", то это означает, что объекты класса имеют право находится только в куче(если не перегрузить этот оператор), а это ведет к тому, что нужно закрывать все конструкторы и для генерации объектов сделать статический метод.

    ОтветитьУдалить
  11. По версии VS 2008 Express последний пример будет выглядеть так:
    CBase* b = NULL;
    0113671E mov dword ptr [b],0
    b->printBase();
    01136725 mov ecx,dword ptr [b]
    01136728 call CBase::printBase (1133D34h)

    Т.е. загрузили нуль и вызвали функцию под названием CBase::printBase, которая безболезненно отработала.
    С virtual да, работать не будет. Упадёт при попытке получить содержимое по нулевому указателю, откуда оно по смещению в таблице виртуальных функций пытается получить адрес printBase. (В данном случае у нас функция первая в списке, поэтому смещений нет. Если добавить что-то такое:
    virtual void stub() {;}
    то в листинге появится такая строчка:
    0116D70F mov eax,dword ptr [edx+4]
    )
    P.S. Если наговорил бред - поправьте. В ассемблере, как и в C++ не силён. Просто очень заинтриговало.

    ОтветитьУдалить
  12. 2Алёна Верно, но дело в том, что архитектура продумывается заранее и тут то и можно избавится от таких конструкций, на то и есть идиомы. Конечно, когда поддерживаем старый код, то тут деватся некуда.

    Авто счетчики дело хорошее, но shared_ptr лучше. Вообще вру, сталкивался с таким, но только у коллеги, и код, надо сказать, приводил к весьма скрытым багам, так как он сам иногда забывал что есть объект который вдруг сам себя может удалить :)

    ОтветитьУдалить
  13. ну и очевидное - экземпляры класса должны быть созданы на куче :)
    class CBase
    {
    int m_i;
    public:
    void MyFunction();
    };

    void CBase::MyFunction()
    {
    delete this;
    }

    int main (){
    CBase a, *b = new CBase();
    b->MyFunction();
    a.MyFunction();//run time error
    }

    ОтветитьУдалить
  14. Анонимный5/5/09 11:19

    (возможно не к месту)
    Конструкция не всегда применима. Что если объект класса размещен в статической области памяти. Как проверить доступен ли this в этом случае?

    ОтветитьУдалить
  15. В прикладном коде этот фокус чаще всего применять не надо. Он - для библиотек, причём таких, где автор очень чётко понимает, что и зачем он написал (либо это сам разработчик компилятора, либо люди "глубоко в теме", вроде boost).

    Это приём такого же сорта, как, например, и "деструктор на месте":

    this->Object::~Object();

    ОтветитьУдалить
  16. В DLL такая конструкция применяется повсемесно для изоляции рантайма. Экспортируется фукнция, которая создает экземпляр чисто виртуального класса со скрытыми конструкотором и деструктором. Обычно присутствует функция release, которая в классе реализации содержит как раз delete this.

    ОтветитьУдалить
  17. Анонимный5/5/09 15:07

    Вкупе с delete this можно использовать private деструктор - он некоторые проблемы убирает.

    А для коллекции могу подкинуть более интересную семантику
    new (this) MyClass()

    ОтветитьУдалить
  18. размещающий оператор new у Саттера вроде был описан неплохо

    ОтветитьУдалить
  19. 2Анонимный:(возможно не к месту)
    Конструкция не всегда применима. Что если объект класса размещен в статической области памяти. Как проверить доступен ли this в этом случае?

    Угу, не всегда применима. Забота о том, чтобы объект был создан с помощью new, лежит на плечах программиста.

    ОтветитьУдалить
  20. На мой взгляд - это вполне естественная конструкция для С++. Об этом во многих книгах написано.
    Конечно С++ сложен и требует тщательного подхода к проектированию и программированию, но в этом его гибкость и универсальность.
    Я не вижу причин пугаться такой конструкции. Она опасна, но может быть применима :) .

    ОтветитьУдалить
  21. Анонимный20/5/09 13:32

    /* ill-formed program */

    class CBase
    {
    public;
    void release()
    {
    delete this;
    }
    };

    CBase * items= new CBase[10];
    items[5].release();
    delete [] items;

    // Sm0ke \\

    ОтветитьУдалить
  22. Такая конструкция очень часто встречается в интерфейсных библиотеках в Symbian для запуска всяких диалогов и т.п. Потому как после завершения диалога он больше не нужен, и нужно скорее освобождать память, которой немного в мобилах. А ограничения на такие конструкции соблюдаются правилами, по которым построены все классы в Symbian.

    ОтветитьУдалить
  23. Анонимный27/10/09 16:33

    delete this;
    Используется в объектах с подсчетом ссылок. Каждый объект хранит значение, сколько его экземпляров используется. При создании/копировании счетчик увеличивается, а при удалении (вызов деструктора) уменьшается. При достижении 0, объект удаляет сам себя и сразу же покидает деструктор.
    Об этом хорошо написано у Скота Мейерса.

    ОтветитьУдалить
  24. Анонимный7/8/10 22:51

    Я знаю применение: когда нужно "подменить" один объект другим, и при этом сделать это нужно методом этого класса. Например, есть класс А и его метод "А* А::В();", который должен удалить текущий объект (delete this) и создать новый, вернув указатель на него. Но лучше использовать класс-контейнер для такого класса.

    ОтветитьУдалить
  25. Анонимный10/10/10 21:06

    А как быть в случае когда скажем есть обьект который управляет окном.По закрытию окна оконной процедуре приходит сообщение destroy.
    Как самоудалить обьект если не через delete this

    ОтветитьУдалить
  26. Анонимный

    А как быть в случае когда скажем есть обьект который управляет окном.По закрытию окна оконной процедуре приходит сообщение destroy.
    Как самоудалить обьект если не через delete this


    Можно не самоудалять, а сделать некий следящий класс, который будет смотреть на, скажем, флаг, выставленный в объекте и удалять такие объекты раз в N секунд.

    Да вообще можно и delete this использовать, только осторожно :-)

    ОтветитьУдалить
  27. абсолютно нормальная конструкция. Как вы думаете сделан Release в каком-нить Com объекте? Ну а про то что ногу можно отстрелить, если неправильно воспользоваться это известный факт. Взять тот же пресловутый shallow copy c указателями на память внутри объектов.

    ОтветитьУдалить
  28. Анонимный7/8/12 14:59

    delete this; вполне нормальная инструкция. Просто нужно понимать зачем она и использовать соотв. Пример реального применения для подсчета ссылок на инстансы объектов в одной кросплатформенной библиотеке http://irrlicht.svn.sourceforge.net/viewvc/irrlicht/trunk/include/IReferenceCounted.h?revision=4267&view=markup

    ОтветитьУдалить
  29. Анонимный23/1/14 12:34

    Конструкция не нужна в "полностью готовой" работе.
    А в качестве заглушки, пока не написан более правильный внешний кусочек кода (который удалит объект и занулит указатель) - вполне нормальная штука. Например, когда пишется новый класс и хочется его потестить в неспецифичном участке программы.

    ОтветитьУдалить
  30. Я использовал данную конструкцию в функции класса, выделяющей память. В случае её отказа необходимо удалить целиком объект и произвести инициализацию заново, иначе последствия могут стать очень интересными.

    ОтветитьУдалить