Должны ли объекты удалять себя в C ++? - PullRequest
24 голосов
/ 07 февраля 2009

Последние 4 года я провел в C #, поэтому меня интересуют текущие передовые практики и общие шаблоны проектирования в C ++. Рассмотрим следующий частичный пример:

class World
{
public:
    void Add(Object *object);
    void Remove(Object *object);
    void Update();
}

class Fire : Object
{
public:
    virtual void Update()
    {
        if(age > burnTime)
        {
            world.Remove(this);
            delete this;
        }
    }
}

Здесь у нас есть мир, ответственный за управление набором объектов и их регулярное обновление. Огонь - это объект, который может быть добавлен в мир при многих других обстоятельствах, но обычно другим объектом, уже существующим в мире. Огонь - единственный объект, который знает, когда он сгорел, поэтому в настоящее время я удаляю его сам. Объект, который создал огонь, вероятно, больше не существует или не имеет отношения к делу.

Разумно ли это делать или есть лучший дизайн, который поможет очистить эти объекты?

Ответы [ 11 ]

30 голосов
/ 07 февраля 2009

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

1.) Переопределенная реализация может не забыть сделать World.Remove, но может забыть delete this. Утечка памяти.

2.) Переопределенная реализация вызывает базовый класс Update, который выполняет delete this, но затем выполняет дополнительную работу, но с недопустимым указателем this.

Рассмотрим этот пример:

class WizardsFire: public Fire
{
public:
    virtual void Update()
    {
        Fire::Update();  // May delete the this-object!
        RegenerateMana(this.ManaCost); // this is now invaild!  GPF!
    }
}
20 голосов
/ 07 февраля 2009

Проблема в том, что вы действительно создаете неявную связь между объектом и классом World.

Если я попытаюсь вызвать Update () вне класса World, что произойдет? Я мог бы в конечном итоге удалить объект, и я не знаю почему. Кажется, обязанности плохо перепутаны. Это вызовет проблемы в тот момент, когда вы используете класс Fire в новой ситуации, о которой вы не думали, когда писали этот код. Что произойдет, если объект должен быть удален из более чем одного места? Возможно, его следует удалить как из мира, с текущей карты, так и из инвентаря игрока? Ваша функция обновления удалит его из мира, а затем удалит объект, и в следующий раз, когда карта или инвентарь попытается получить доступ к объекту, произойдут плохие вещи.

В общем, я бы сказал, что функция Update () не очень интуитивно удаляет объект, который она обновляет. Я также сказал бы, что для объекта нет смысла удалять себя. Скорее всего, у объекта должен быть какой-то способ инициировать событие, сообщающее, что оно сгорело, и теперь любой может заинтересоваться. Например, удалив его из мира. Чтобы удалить его, подумайте о владении.

Кому принадлежит объект? Мир? Это означает, что только мир может решить, когда объект умрет. Это хорошо, если мировая ссылка на объект будет переживать другие ссылки на него. Как вы думаете, объект сам по себе? Что это хотя бы значит? Объект должен быть удален, когда объект больше не существует? Не имеет смысла.

Но если нет четко определенного единственного владельца, реализуйте общее владение, например, используя интеллектуальный указатель, реализующий подсчет ссылок, такой как boost::shared_ptr

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

6 голосов
/ 07 февраля 2009

Удаление завершится неудачно и может вызвать сбой программы, если объект был не выделен и создан с новым. Эта конструкция не даст вам создать экземпляр класса в стеке или статически. В вашем случае вы, похоже, используете какую-то схему распределения. Если вы придерживаетесь этого, это может быть безопасно. Я бы точно не стал этого делать.

6 голосов
/ 07 февраля 2009

Нет ничего плохого в том, что объекты удаляются в C ++, большинство реализаций подсчета ссылок будут использовать нечто подобное Тем не менее, это выглядит как немного эзотерическая техника, и вам нужно будет внимательно следить за вопросами собственности.

5 голосов
/ 07 февраля 2009

Это, безусловно, работает для объектов, созданных new, и когда вызывающий Update должным образом информируется об этом поведении. Но я бы избежал этого. В вашем случае право собственности явно принадлежит миру, поэтому я бы заставил мир удалить его. Объект не создается сам, я думаю, что он также не должен удалять себя. Может быть очень удивительно, если вы вызываете функцию «Обновление» для вашего объекта, но вдруг этот объект больше не существует, когда Мир ничего не делает из себя (не считая удаления его из своего списка - но в другом кадре! Вызов кода). Обновление об объекте этого не заметит).

Некоторые идеи на этот счет

  1. Добавить список ссылок на объекты в World. Каждый объект в этом списке ожидает удаления. Это распространенная техника, которая используется в wxWidgets для окон верхнего уровня, которые были закрыты, но все еще могут получать сообщения. Во время простоя, когда все сообщения обрабатываются, список ожидания обрабатывается, и объекты удаляются. Я верю, что Qt следует аналогичной технике.
  2. Скажите миру, что объект хочет быть удаленным. Мир будет должным образом информирован и позаботится обо всем, что нужно сделать. Что-то вроде deleteObject(Object&) возможно.
  3. Добавьте функцию shouldBeDeleted к каждому объекту, которая возвращает истину, если объект желает быть удаленным его владельцем.

Я бы предпочел вариант 3. Мир назвал бы Обновление. И после этого он проверяет, должен ли объект быть удален, и может ли он сделать это - или, если пожелает, запоминает этот факт, добавляя этот объект в список отложенных удалений вручную.

Боль в заднице, когда вы не можете быть уверены, когда и когда не сможете получить доступ к функциям и данным объекта. Например, в wxWidgets есть класс wxThread, который может работать в двух режимах. Один из этих режимов (называемый «отсоединяемым») заключается в том, что если его основная функция возвращает (и ресурсы потока должны быть освобождены), она удаляет себя (для освобождения памяти, занятой объектом wxThread), вместо того, чтобы ждать владельца потока объект для вызова функции ожидания или соединения. Однако это вызывает сильную головную боль. Вы никогда не можете вызывать какие-либо функции для него, потому что он мог быть прерван при любых обстоятельствах, и вы не можете создать его без использования new. Некоторые люди говорили мне, что им очень не нравится это поведение.

Самостоятельное удаление объекта с подсчетом ссылок пахнет, imho. Давайте сравним:

// bitmap owns the data. Bitmap keeps a pointer to BitmapData, which
// is shared by multiple Bitmap instances. 
class Bitmap {
    ~Bitmap() {
        if(bmp->dec_refcount() == 0) {
            // count drops to zero => remove 
            // ref-counted object. 
            delete bmp;
        }
    }
    BitmapData *bmp;
};

class BitmapData {
    int dec_refcount();
    int inc_refcount();

};

Сравните это с самоуничтожающимися пересчитанными объектами:

class Bitmap {
    ~Bitmap() {
        bmp->dec_refcount();
    }
    BitmapData *bmp;
};

class BitmapData {
    int dec_refcount() {
        int newCount = --count;
        if(newCount == 0) {
            delete this;
        }
        return newCount;
    }
    int inc_refcount();
};

Я думаю, что первое намного приятнее, и я считаю, что хорошо спроектированные объекты с подсчетом ссылок не действительно "удаляют это", потому что это увеличивает связь: класс, использующий данные с подсчетом ссылок, должен знать и помните о том, что данные удаляются как побочный эффект уменьшения числа ссылок. Обратите внимание, что «bmp» становится, вероятно, висящим указателем в деструкторе ~ Bitmap. Возможно, не делать это «удалить это» гораздо приятнее здесь.

Ответ на аналогичный вопрос «Какая польза от удаления этого» * ​​1032 *

5 голосов
/ 07 февраля 2009

Я предпочитаю более строгую модель владения. Мир должен владеть объектами в нем и нести ответственность за их очистку. Либо сделайте так, чтобы world remove сделала это, либо попросите update (или другую функцию) вернуть значение, указывающее, что объект должен быть удален.

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

2 голосов
/ 07 февраля 2009

Другие упоминали о проблемах с «удалить это». Короче говоря, поскольку «Мир» управляет созданием Огня, он также должен управлять его удалением.

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

Для семантики, которую вы хотите, у меня был бы флаг "активный" или "живой" в вашем классе "Огонь" (или объект, если применимо). Тогда мир будет иногда проверять свой инвентарь объектов и избавляться от тех, которые больше не активны.

-

Еще одно замечание: у вашего написанного кода есть Fire, приватно наследуемый от Object, поскольку это значение по умолчанию, хотя оно гораздо реже, чем публичное наследование. Вам, вероятно, следует сделать явное наследование, и я подозреваю, что вы действительно хотите публичное наследование.

2 голосов
/ 07 февраля 2009

Это довольно распространенная реализация подсчета ссылок, и я успешно использовал ее раньше.

Тем не менее, я также видел его сбой. Хотел бы я вспомнить обстоятельства аварии. @ abelenky это место, где я видел, как он рухнул.

Возможно, именно там вы подклассом Fire, но не можете создать виртуальный деструктор (см. Ниже). Если у вас нет виртуального деструктора, функция Update() будет вызывать ~Fire() вместо соответствующего деструктора ~Flame().

class Fire : Object
{
public:
    virtual void Update()
    {
        if(age > burnTime)
        {
            world.Remove(this);
            delete this; //Make sure this is a virtual destructor.
        }
    }
};

class Flame: public Fire
{
private: 
    int somevariable;
};
1 голос
/ 07 февраля 2009

Как правило, не рекомендуется делать «удалить это», если в этом нет необходимости или используется очень простым способом. В вашем случае, похоже, что Мир может просто удалить объект при его удалении, если нет других зависимостей, которые мы не видим (в этом случае ваш вызов удаления вызовет ошибки, так как они не уведомлены). Сохранение права собственности вне вашего объекта позволяет лучше инкапсулировать ваш объект и сделать его более стабильным: объект не станет недействительным в какой-то момент просто потому, что он решил удалить себя.

0 голосов
/ 07 февраля 2009

удалить это очень рискованно. В этом нет ничего плохого. Это законно. Но есть так много вещей, которые могут пойти не так, когда вы используете его, что он должен использоваться экономно.

Рассмотрим случай, когда у кого-то есть открытые указатели или ссылки на объект, а затем он уходит и удаляет себя.

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

...