Как иметь дело с различными стратегиями владения для члена-указателя?

Рассмотрим следующую структуру класса:

class Filter
{
    virtual void filter() = 0;
    virtual ~Filter() { }
};

class FilterChain : public Filter
{
    FilterChain(collection<Filter*> filters)
    {
         // copies "filters" to some internal list
         // (the pointers are copied, not the filters themselves)
    }

    ~FilterChain()
    {
         // What do I do here?
    }

    void filter()
    {
         // execute filters in sequence
    }
};

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

В настоящее время у меня есть некоторые проблемы с дизайном, связанные с владением Filter объектами, на которые FilterChain держит указатели. В частности, вот два возможных сценария использования FilterChain:

  • Сценарий A: некоторые функции в моей библиотеке создают (возможно, сложную) цепочку фильтров, выделяют память по мере необходимости и возвращают вновь выделенный объект FilterChain. Например, одна из этих функций строит цепочку фильтров из файла, который может описывать произвольно сложные фильтры (включая цепочки фильтров цепочек фильтров и т. д.). Пользователь функции несет ответственность за уничтожение объекта после завершения работы.
  • Сценарий B: пользователь имеет доступ к набору Filter объектов и хочет определенным образом объединить их в цепочки фильтров. Пользователь создает FilterChain объектов для собственного использования, а затем уничтожает их, когда он с ними покончил. Объекты Filter не должны уничтожаться при уничтожении FilterChain, ссылающегося на них.

Теперь два самых простых способа управлять владением объектом FilterChain:

  • FilterChain владеет Filter объектами. Это означает, что объекты, на которые ссылается FilterChain, уничтожаются в деструкторе FilterChain. Что несовместимо со сценарием Б.
  • FilterChain не владеет Filter объектами. Это означает, что деструктор FilterChain ничего не делает. Теперь есть проблема со сценарием A, потому что пользователь должен знать внутреннюю структуру всех задействованных объектов Filter, чтобы уничтожить их все, не пропустив ни одного, поскольку родитель FilterChain не делает этого сам. Это просто плохой дизайн и запрос на утечку памяти.

Следовательно, мне нужно что-то более сложное. Мое первое предположение состоит в том, чтобы разработать интеллектуальный указатель с устанавливаемым логическим флагом, указывающим, владеет ли интеллектуальный указатель объектом. Тогда вместо набора указателей на Filter объектов FilterChain будет принимать набор интеллектуальных указателей на Filter объектов. Когда вызывается деструктор FilterChain, он уничтожает умные указатели. Затем деструктор самого интеллектуального указателя уничтожит объект, на который указывает указатель (объект Filter) если и только если установлен логический флаг, указывающий на принадлежность.

У меня такое ощущение, что эта проблема распространена в C++, но мои поиски в Интернете популярных решений или умных шаблонов проектирования не увенчались успехом. На самом деле, auto_ptr здесь не очень помогает, а shared_ptr кажется излишним. Итак, мое решение является хорошей идеей или нет?


person Etienne Dechamps    schedule 28.07.2010    source источник
comment
Почему общий указатель является излишним? Гораздо проще, чем накатывать свои собственные в плане немедленной реализации и сопровождения (каждый точно знает, что они из себя представляют). Накладные расходы минимальны.   -  person Patrick    schedule 28.07.2010
comment
Деструктор базового класса ДОЛЖЕН быть виртуальным.   -  person    schedule 28.07.2010
comment
@Patrick: мне кажется, что это излишество, потому что shared_ptr - это счетчик ссылок, а мне нужен только логический флаг.   -  person Etienne Dechamps    schedule 28.07.2010
comment
@ Нил Баттерворт: да, конечно. Я опустил его для краткости.   -  person Etienne Dechamps    schedule 28.07.2010
comment
Но не является ли написание и тестирование полного класса интеллектуальных указателей более излишним, чем использование предварительно протестированного класса, который использует int вместо bool? Также достаточно вероятно, что реализация std::shared_ptr будет лучше (размер кода, скорость и т. д.), чем моя первая попытка класса интеллектуальных указателей...   -  person Patrick    schedule 29.07.2010
comment
@ e-t172: Это не кратко, это ключевая информация.   -  person    schedule 29.07.2010


Ответы (4)


Интеллектуальные указатели здесь не излишни: очевидно, у вас есть проблема проектирования, которая так или иначе требует тщательного рассмотрения времени жизни объекта и владения им. Это особенно актуально, если вам нужна возможность переназначать фильтры в графе фильтров во время выполнения или возможность создавать составные объекты FilterChain.

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

Могу поспорить, что время выполнения этапов фильтрации будет намного больше дополнительных накладных расходов на разыменование интеллектуального указателя. shared_ptr разработан, чтобы быть довольно легким.

person the_mandrill    schedule 28.07.2010
comment
Кажется, все здесь думают, что это правильно. Хорошо, тогда давайте перейдем к shared_ptr. - person Etienne Dechamps; 29.07.2010
comment
Раньше у меня был небольшой ментальный блок, когда дело дошло до интеллектуальных указателей, но, начав использовать shared_ptr совсем недавно, я обнаружил, что это настолько упрощает жизнь. - person the_mandrill; 29.07.2010

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

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

РЕДАКТИРОВАТЬ: Как заметил Нил, Filter нужен виртуальный деструктор.

person Mark B    schedule 28.07.2010
comment
Объекты фильтра нельзя копировать не из-за проблем с памятью, а потому, что это вызовет проблемы, связанные с дублированием информации о состоянии (также это будет сомнительно семантически). - person Etienne Dechamps; 28.07.2010
comment
@ e-t172 Тогда как насчет сохранения состояния фильтра в shared_ptr, чтобы вы могли скопировать Filter s? - person Mark B; 28.07.2010
comment
В чем разница с использованием shared_ptr в FilterChain? - person Etienne Dechamps; 29.07.2010
comment
@ e-t172 Просто меняется место подсчета ссылок. Если вы разрешите объектам Filter иметь состояние с подсчетом ссылок, то вы сможете копировать их по желанию, полностью избавляя клиентов от необходимости вообще создавать shared_ptr для фильтра: они могут просто пройти в коллекцию и понять, что они все еще владеют все фильтры в коллекции И что Filtercontainer правильно очистит все свои фильтры, когда закончит. - person Mark B; 29.07.2010

FilterChain должен иметь отдельный метод DeleteAll(), который итерирует коллекцию и delete фильтрует. Он вызывается в сценарии A и не вызывается в сценарии B. Это требует некоторого интеллекта со стороны пользователей FilterChain, но не более чем запоминание delete и объект, который они new'd. (Они должны быть в состоянии справиться с этим, иначе они заслуживают утечки памяти)

person James Curran    schedule 28.07.2010
comment
Они не должны должны помнить об удалении чего-либо и где угодно. Используйте контейнеры, интеллектуальные указатели и т. д. Будьте защищены от исключений, будьте чистыми, никогда явно не управляйте вещами вне служебного класса. - person GManNickG; 28.07.2010

Я бы предпочел, чтобы FilterChain не владел объектами Filter. Затем в вашей библиотеке, когда вам нужно загрузить FilterChain из файла, у вас будет другой объект Loader, который отвечает за время жизни объектов Filter. Таким образом, FilterChain будет работать согласованно для цепочек, загруженных библиотекой, и цепочек, созданных пользователем.

person Chad Simpkins    schedule 28.07.2010
comment
Это означало бы дублирование возможно сложной структуры фильтра в объект Loader только для управления владением. Это слишком сложно по сравнению с другими решениями. - person Etienne Dechamps; 29.07.2010
comment
Фактически то, что вы предлагаете, является просто вторым решением в моем первоначальном вопросе, за исключением того, что вы перемещаете логику управления объектами фильтра извне библиотеки внутрь. Действительно код проще для пользователя библиотеки, но то, что чрезмерная сложность скрыта внутри библиотеки, не означает, что это хороший дизайн (это необходимо, но недостаточно). - person Etienne Dechamps; 29.07.2010
comment
Я не понимаю, зачем вам нужно дублировать структуру фильтра в загрузчике. Загрузчику нужно будет только управлять временем жизни фильтров. Пользователь библиотеки по-прежнему должен будет управлять сроком службы своих собственных фильтров. Фильтры должны (де)сериализоваться. Всякий раз, когда я вижу подобный код, это происходит потому, что при проектировании класса Filter не было уделено должного внимания, и теперь вы расплачиваетесь за это. - person Chad Simpkins; 29.07.2010
comment
Я не понимаю, зачем вам нужно дублировать структуру фильтра в загрузчике. Загрузчику нужно будет только управлять временем жизни фильтров. Я согласен. Дело в том, что фильтр может быть простым фильтром, или цепочкой фильтров, или цепочкой цепочек и т. д., и мы только что вернулись к исходной проблеме. - person Etienne Dechamps; 29.07.2010