Должны ли объекты удалять себя в C++?

Я провел последние 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 ответов

Решение

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

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

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

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

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

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

Вы сделали 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!
    }
}

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

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

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

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

Это, безусловно, работает для объектов, созданных 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. Возможно, не делать это "удалить это" гораздо приятнее здесь.

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

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

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

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

-

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

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

Тем не менее, я также видел его сбой. Хотел бы я вспомнить обстоятельства аварии. @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;
};

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

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

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

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

Я не думаю, что с удалением самого объекта что-то не так, но другой возможный метод состоит в том, чтобы весь мир отвечал за удаление объектов как часть::Remove (при условии, что все Удаленные объекты также были удалены).

Другие вопросы по тегам