19

我有在其构造函数中创建其他子对象的对象,传递“this”以便子对象可以将指针保存回其父对象。我在编程中广泛使用 boost::shared_ptr 作为 std::auto_ptr 或原始指针的更安全替代方案。所以孩子会有诸如 , 之类的代码shared_ptr<Parent>,而 boost 提供了shared_from_this()父母可以给孩子的方法。

我的问题是它shared_from_this()不能在构造函数中使用,这并不是真正的犯罪,因为除非您知道自己在做什么并且不介意限制,否则无论如何都不应该在构造函数中使用“this”。

Google 的 C++ Style Guide声明构造函数只应将成员变量设置为其初始值。任何复杂的初始化都应该在显式的 Init() 方法中进行。这解决了“this-in-constructor”问题以及其他一些问题。

困扰我的是,现在使用您的代码的人必须记住每次构造您的对象时都调用 Init()。我能想到的唯一方法是通过断言 Init() 已经在每个成员函数的顶部被调用,但这写起来很乏味,执行起来也很麻烦。

在此过程中的任何步骤中是否有任何成语可以解决此问题?

4

6 回答 6

16

使用工厂方法两阶段构造和初始化您的类,然后将 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;
}

编辑:

如果您希望对象存在于堆栈中,您可以使用上述模式的变体。如所写,这将创建一个临时并使用复制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;
}
于 2010-03-24T18:53:20.193 回答
9

Google 的 C++ 编程指南在这里和其他地方一次又一次地受到批评。没错。

只有当它隐藏在包装类后面时,我才会使用两阶段初始化。如果手动调用初始化函数可行,我们仍然会使用 C 和 C++ 进行编程,其构造函数永远不会被发明。

于 2010-03-24T18:53:12.417 回答
5

根据情况,这可能是共享指针不添加任何内容的情况。它们应该在生命周期管理出现问题的任何时候使用。如果保证子对象的生命周期比父对象的生命周期短,我认为使用原始指针没有问题。例如,如果父对象创建和删除子对象(而其他人没有这样做),则不存在谁应该删除子对象的问题。

于 2010-03-24T19:21:13.623 回答
3

KeithB 有一个非常好的观点,我想扩展(在某种意义上与问题无关,但不适合评论):

在对象与其子对象的关系的特定情况下,生命周期是有保证的:父对象总是比子对象寿命长。在这种情况下,子(成员)对象共享父(包含)对象的所有权,shared_ptr不应使用 a。不应出于语义原因(根本没有共享所有权)或出于实际原因使用它:您可能会引入各种问题:内存泄漏和不正确的删除。

为了便于讨论,我将使用P指代父对象和C指代子对象或包含对象。

如果P生命周期是用 a 外部处理的shared_ptr,那么添加另一个shared_ptrinC来引用P将具有创建循环的效果。一旦你有一个由引用计数管理的内存循环,你很可能会发生内存泄漏:当最后一个shared_ptr引用的外部P超出范围时,指针C仍然是活动的,所以引用计数P没有达到 0 并且对象不会被释放,即使它不再可访问。

如果P由不同的指针处理,那么当指针被删除时,它将调用P析构函数,这将级联调用C析构函数。that hasP中的引用计数将达到 0,它将触发双重删除。shared_ptrC

如果P具有自动存储持续时间,当它的析构函数被调用(对象超出范围或包含对象的析构函数被调用)时,shared_ptr将触发删除未新建的内存块。

常见的解决方案是用 s 打破循环weak_ptr,这样子对象就不会将 a 保留shared_ptr给父对象,而是保留 a weak_ptr。在这个阶段,问题是一样的:要创建 aweak_ptr对象必须已经由 a 管理shared_ptr,这在构造过程中不会发生。

考虑使用原始指针(通过指针处理资源的所有权是不安全的,但这里的所有权是在外部处理的,所以这不是问题)甚至是引用(这也告诉其他程序员您信任引用的对象P寿命更长引用对象C

于 2010-03-24T20:01:49.253 回答
0

在这种情况下你真的需要使用 shared_ptr 吗?孩子可以有一个指针吗?毕竟,它是子对象,所以它是由父对象拥有的,所以它不能只有一个指向它的父对象的普通指针吗?

于 2010-03-24T20:05:34.957 回答
0

需要复杂构造的对象听起来像是工厂的工作。

定义一个无法构造的接口或抽象类,以及一个可能带有参数的自由函数,该函数返回一个指向该接口的指针,但在幕后负责处理复杂性。

您必须根据班级的最终用户必须做什么来考虑设计。

于 2010-03-24T19:00:18.570 回答