Управление временем жизни инкапсулированных объектов - PullRequest
2 голосов
/ 12 января 2009

Каков наилучший подход для инкапсуляции объектов и управления их временем жизни? Пример: у меня есть класс A , который содержит объект типа B и несет ответственность за него .

Решение 1 , клон b объекта, чтобы гарантировать, что только A может очистить его.

class A
{
    B *b;
public:
    A(B &b)
    {
        this->b = b.clone();
    }

    ~A()
    {
        delete b; // safe
    }
};

Решение 2 , напрямую используйте переданный объект, мы рискуем получить здесь потенциальную двойную свободу.

class A
{
    B *b;
public:
    A(B *b)
    {
        this->b = b;
    }

    ~A()
    {
        delete b; // unsafe
    }
};

В моем случае решение № 2 подошло бы лучше всего. Однако мне интересно, считается ли это плохим кодом, потому что кто-то может не знать о поведении A , даже если это задокументировано. Я могу думать об этих сценариях:

B *myB = new B();
A *myA = new A(myB);
delete myB; // myA contains a wild pointer now

Или,

B *myB = new B();
A *firstA = new A(myB);
A *secondA = new A(myB); // bug! double assignment
delete firstA; // deletes myB, secondA contains a wild pointer now
delete secondA; // deletes myB again, double free

Могу ли я просто игнорировать эти проблемы, если правильно документирую поведение A? Достаточно ли объявить об ответственности и предоставить остальным возможность читать документы? Как это управляется в вашей кодовой базе?

Ответы [ 6 ]

5 голосов
/ 12 января 2009

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

Умные указатели - ваш друг. std::auto_ptr<> является вашим другом, когда один объект владеет другим и отвечает за его удаление при выходе из области видимости. boost::shared_ptr<> (или, теперь, std::tr1::shared_ptr<>) - ваш друг, когда к другому объекту потенциально может быть подключено более одного объекта, и вы хотите удалить объект, если на него больше нет ссылок.

Итак, используйте ваше решение 1 с auto_ptr или ваше решение 2 с shared_ptr.

4 голосов
/ 12 января 2009

Вы должны определить свой объект так, чтобы семантика владения была, насколько это возможно, определена интерфейсом. Как отметил Дэвид Торнли, std :: auto_ptr - это умный указатель выбора для указания передачи права собственности . Определите свой класс так:

class A
{    
    std::auto_ptr<B> b;
public:    
    A(std::auto_ptr<B> b)    
    {
        this->b = b;
    }
    // Don't need to define this for this scenario
    //~A()
    //{ 
    //   delete b; // safe
    //}
};

Поскольку контракт std :: auto_ptr - это присвоение = передача права собственности, ваш конструктор теперь неявно заявляет, что объект A владеет указателем на B, который он передал. Фактически, если клиент пытается сделать что-то с std :: auto_ptr , который он использовал для построения A после построения, операция завершится неудачей, так как указатель, который он содержит, будет недействительным.

2 голосов
/ 12 января 2009

Если ваш объект несет полную ответственность за переданный объект, то удаление его должно быть безопасным. Если это небезопасно, то утверждение о том, что вы несете полную ответственность, является ложным. Так что это? Если в вашем интерфейсе задокументировано, что вы БУДЕТЕ удалить входящий объект, то вызывающая сторона должна убедиться, что вы получили объект, который вы должны удалить.

2 голосов
/ 12 января 2009

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

Когда экземпляру инкапсулирующего класса назначается объект B, он вызывает метод для увеличения счетчика ссылок B объекта. Когда инкапсулирующий класс уничтожается, он не удаляет B, а вместо этого вызывает метод, уменьшающий количество ссылок. Когда счетчик достигает нуля, объект B уничтожается (или уничтожается сам по себе). Таким образом, несколько экземпляров инкапсулирующего класса могут работать с одним экземпляром объекта B.

Больше по теме: Подсчет ссылок .

1 голос
/ 12 января 2009

Если вы клонируете A, и оба A1 и A2 сохраняют ссылки на B, то время жизни B не контролируется полностью A. Он распределяется между различными A. Клонирование B обеспечивает взаимно-однозначное отношение между Как и Bs, что позволит легко обеспечить долговечность.

Если клонирование B не является вариантом, вам необходимо отказаться от концепции, согласно которой A отвечает за время жизни B. Либо другой объект должен будет управлять различными B, либо вам потребуется реализовать метод, такой как подсчет ссылок.

Для справки, когда я думаю о термине «Клон», он подразумевает глубокую копию, которая также клонировала бы В. Я ожидаю, что два As будут полностью отделены друг от друга после клона.

0 голосов
/ 12 января 2009

Я не клонирую вещи без необходимости или «просто чтобы быть в безопасности».

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

...