понедельник, мая 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 коммент.:

reperio комментирует...

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

Илья Кулаков комментирует...

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

Анонимный комментирует...

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

Unknown комментирует...

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

Анонимный комментирует...

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

Александр комментирует...

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

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

Анонимный комментирует...

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

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

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

Анонимный комментирует...

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

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

Alena комментирует...

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

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

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

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

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

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

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

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

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

Unknown комментирует...

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

nekto0n комментирует...

По версии 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++ не силён. Просто очень заинтриговало.

reperio комментирует...

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

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

Yuri Volkov комментирует...

ну и очевидное - экземпляры класса должны быть созданы на куче :)
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
}

Анонимный комментирует...

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

Unknown комментирует...

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

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

this->Object::~Object();

Kirill V. Lyadvinsky комментирует...

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

Анонимный комментирует...

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

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

Илья Кулаков комментирует...

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

Alena комментирует...

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

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

Vladimir Ivanov комментирует...

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

Анонимный комментирует...

/* ill-formed program */

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

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

// Sm0ke \\

Symbian комментирует...

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

Анонимный комментирует...

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

Анонимный комментирует...

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

Анонимный комментирует...

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

Alena комментирует...

Анонимный

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


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

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

Denis Sotnikov комментирует...

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

Анонимный комментирует...

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

Анонимный комментирует...

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

Unknown комментирует...

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