Как обработать указатель this в конструкторе? - PullRequest
18 голосов
/ 24 марта 2010

У меня есть объекты, которые создают другие дочерние объекты внутри своих конструкторов, передавая «this», чтобы дочерний элемент мог сохранить указатель на своего родителя. Я широко использую boost :: shared_ptr в своем программировании как более безопасную альтернативу std :: auto_ptr или необработанным указателям. Таким образом, у ребенка будет такой код, как shared_ptr<Parent>, а boost предоставляет метод shared_from_this(), который родитель может дать ребенку.

Моя проблема в том, что shared_from_this() нельзя использовать в конструкторе, что на самом деле не является преступлением, потому что «это» не должно использоваться в конструкторе в любом случае, если вы не знаете, что делаете и не против ограничения.

Руководство по стилю Google C ++ гласит , что конструкторы должны просто устанавливать переменные-члены в их начальные значения. Любая сложная инициализация должна идти в явном методе Init (). Это решает проблему «это в конструкторе», а также некоторые другие.

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

Существуют ли идиомы, которые решают эту проблему на любом этапе пути?

Ответы [ 6 ]

16 голосов
/ 24 марта 2010

Используйте фабричный метод для двухфазного конструирования и инициализации вашего класса, а затем сделайте функцию ctor & Init () закрытой. Тогда нет способа создать ваш объект неправильно. Просто не забывайте держать деструктор открытым и использовать умный указатель:

#include <memory>

class BigObject
{
public:
    static std::tr1::shared_ptr<BigObject> Create(int someParam)
    {
        std::tr1::shared_ptr<BigObject> ret(new BigObject(someParam));
        ret->Init();
        return ret;
    }

private:
    bool Init()
    {
        // do something to init
        return true;
    }

    BigObject(int para)
    {
    }

    BigObject() {}

};


int main()
{
    std::tr1::shared_ptr<BigObject> obj = BigObject::Create(42);
    return 0;
}

EDIT:

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

#include <memory>

class StackObject
{
public:
    StackObject(const StackObject& rhs)
        : n_(rhs.n_)
    {
    }

    static StackObject Create(int val)
    {
        StackObject ret(val);
        ret.Init();
        return ret;
    }
private:
    int n_;
    StackObject(int n = 0) : n_(n) {};
    bool Init() { return true; }
};

int main()
{
    StackObject sObj = StackObject::Create(42);
    return 0;
}
8 голосов
/ 24 марта 2010

Руководство Google по программированию на C ++ снова и снова подвергалось критике здесь и в других местах. И это правильно.

Я использую двухфазную инициализацию только когда она скрыта за классом переноса. Если бы ручной вызов функций инициализации работал, мы все равно программировали бы на C и C ++, а его конструкторы никогда бы не были изобретены.

5 голосов
/ 24 марта 2010

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

3 голосов
/ 24 марта 2010

У KeithB есть действительно хорошее замечание, которое я хотел бы расширить (в смысле, который не связан с вопросом, но который не будет вписываться в комментарий):

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

Чтобы облегчить обсуждение, я буду использовать P для ссылки на родительский объект и C для ссылки на дочерний или содержащийся объект.

Если время жизни P обрабатывается извне с помощью shared_ptr, то добавление еще одного shared_ptr в C для ссылки на P приведет к созданию цикла. Когда у вас есть цикл в памяти, управляемый подсчетом ссылок, вы, скорее всего, будете иметь утечку памяти: когда последний внешний shared_ptr, который ссылается на P, выходит из области видимости, указатель в C все еще жив, поэтому ссылка значение для P не достигает 0, и объект не освобождается, даже если он больше не доступен.

Если P обрабатывается другим указателем, то когда указатель будет удален, он вызовет деструктор P, который будет каскадно вызывать деструктор C. Счетчик ссылок для P в shared_ptr, который есть у C, достигнет 0 и приведет к двойному удалению.

Если P имеет автоматическую продолжительность хранения, когда вызывается его деструктор (объект выходит из области видимости или вызывается деструктор содержащего объекта), тогда shared_ptr инициирует удаление не нового блока памяти -ed.

Распространенным решением является разрыв циклов с weak_ptr с, чтобы дочерний объект не оставлял shared_ptr для родителя, а скорее weak_ptr. На этом этапе проблема та же: для создания weak_ptr объект должен уже управляться shared_ptr, чего во время строительства не может быть.

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

0 голосов
/ 24 марта 2010

Вам действительно нужно использовать shared_ptr в этом случае?Может ли ребенок просто иметь указатель?В конце концов, это дочерний объект, поэтому он принадлежит родительскому объекту, поэтому не может ли он просто иметь нормальный указатель на свой родительский объект?

0 голосов
/ 24 марта 2010

Объект, требующий сложной конструкции, звучит как работа для фабрики.

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

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

...