Как обращаться с указателем 'this' в конструкторе?
У меня есть объекты, которые создают другие дочерние объекты в своих конструкторах, передавая 'this', чтобы ребенок мог сохранить указатель обратно на родителя. Я широко использую boost:: shared_ptr в своем программировании как более безопасную альтернативу std:: auto_ptr или raw указателям. Таким образом, у ребенка будет такой код, как shared_ptr<Parent>
, а boost предоставляет метод shared_from_this()
, который родитель может передать ребенку.
Моя проблема заключается в том, что shared_from_this()
не может использоваться в конструкторе, что на самом деле не является преступлением, потому что 'this' не должно использоваться в конструкторе, если вы не знаете, что делаете, и не возражаете против ограничения.
Руководство по стилю Google С++ указывает, что конструкторы должны просто устанавливать переменные-члены в свои начальные значения. Любая сложная инициализация должна идти в явном методе Init(). Это решает проблему "этот-в-конструкторе" , а также несколько других.
Меня беспокоит то, что люди, использующие ваш код, должны помнить, что каждый раз, когда они создают один из ваших объектов, вы должны вызывать Init(). Единственный способ, который я могу придумать для этого, - утверждать, что Init() уже был вызван в верхней части каждой функции-члена, но это утомительно, чтобы писать и громоздко выполнять.
Есть ли там какие-либо идиомы, которые решают эту проблему на любом этапе?
Ответы
Ответ 1
Используйте метод factory для 2-фазной конструкции и инициализируйте свой класс, а затем сделайте функцию 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;
}
Ответ 2
Руководства по программированию на Google С++ были подвергнуты критике здесь и в другом месте снова и снова. И это правильно.
Я использую двухфазную инициализацию только когда она скрыта за классом упаковки. Если бы ручные функции инициализации работали, мы все равно будем программировать на C и С++, и его конструкторы никогда бы не были изобретены.
Ответ 3
В зависимости от ситуации это может быть случай, когда общие указатели ничего не добавляют. Они должны использоваться в любое время, когда управление жизненным циклом является проблемой. Если срок жизни дочерних объектов будет короче, чем у родителя, я не вижу проблемы с использованием исходных указателей. Например, если родитель создает и удаляет дочерние объекты (и никто другой не делает), нет вопроса, кто должен удалять дочерние объекты.
Ответ 4
У KeithB есть действительно хорошая точка, которую я хотел бы расширить (в некотором смысле, которая не связана с вопросом, но это не соответствует комментарию):
В конкретном случае отношения объекта с его подобъектами гарантируются сроки жизни: родительский объект всегда будет переживать дочерний объект. В этом случае объект child (member) не разделяет права собственности на родительский (содержащий) объект, а shared_ptr
не должен использоваться. Он не должен использоваться по семантическим причинам (без совместного использования вообще) или по практическим причинам: вы можете ввести всевозможные проблемы: утечки памяти и неправильные удаления.
Чтобы облегчить обсуждение, я буду использовать P
для ссылки на родительский объект и C
для ссылки на дочерний объект или содержащий объект.
Если P
время жизни внешне обрабатывается с помощью shared_ptr
, то добавление другого shared_ptr
в C
для ссылки на P
будет иметь эффект создания цикла. Когда у вас есть цикл в памяти, управляемый подсчетом ссылок, у вас, скорее всего, есть утечка памяти: когда последний внешний shared_ptr
, который ссылается на P
выходит за пределы области видимости, указатель в C
все еще жив, поэтому ссылка count для P
не достигает 0, и объект не освобождается, даже если он больше не доступен.
Если P
обрабатывается другим указателем, а затем, когда указатель будет удален, он вызовет деструктор P
, который будет каскадом взывать деструктор C
. Счетчик ссылок для P
в shared_ptr
, который C
будет достигнут 0, и это приведет к двойному удалению.
Если P
имеет автоматическую продолжительность хранения, когда вызван деструктор (объект выходит из области видимости или вызывается вызывающий объект-деструктор), тогда shared_ptr
инициирует удаление блока памяти, который не был новым -ed.
Общее решение - это разбиение циклов на weak_ptr
s, так что дочерний объект не будет содержать shared_ptr
для родителя, а скорее weak_ptr
. На этом этапе проблема одна и та же: для создания weak_ptr
объект уже должен управляться с помощью shared_ptr
, который во время построения не может произойти.
Рассмотрите возможность использования либо необработанного указателя (обработка права собственности на ресурс с помощью указателя небезопасна, но здесь право собственности обрабатывается извне, так что это не проблема) или даже ссылка (которая также говорит другим программистам, что вы доверяете упомянутым object P
, чтобы пережить ссылочный объект C
)
Ответ 5
Объект, который требует сложной конструкции, звучит как задание для factory.
Определите интерфейс или абстрактный класс, который нельзя построить, плюс свободная функция, которая, возможно, с параметрами, возвращает указатель на интерфейс, но за кулисами заботится о сложности.
Вы должны думать о дизайне с точки зрения того, что должен делать конечный пользователь вашего класса.
Ответ 6
Вам действительно нужно использовать shared_ptr в этом случае? Может ли ребенок иметь указатель? В конце концов, это дочерний объект, поэтому он принадлежит родительскому объекту, поэтому он не может иметь нормальный указатель на него родительский?