用于智能指针(intrusive_ptr)的抽象基类 - 处理继承、多态、可克隆性和从工厂方法返回的问题。

20

要求

  1. I am writing a class called RCObject, which stands for "Reference Counted Object";
  2. The class RCObject should be abstract, serving as the base class of a framework (EC++3 Item 7);
  3. Creating instances of RCObject subclasses on the stack should be prohibited (MEC++1 Item 27);

    [ ADDED: ]

    [ Assume Bear is a concrete subclass of RCObject ]

    [ C.E. here means Compilation Error ]

    Bear b1;                        // Triggers C.E. (by using MEC++1 Item 27)
    Bear* b2;                       // Not allowed but no way to trigger C.E.
    intrusive_ptr<Bear> b3;         // Recommended
    
    Bear* bs1 = new Bear[8];                   // Triggers C.E.
    container< intrusive_ptr<RCObject> > bs2;  // Recommended
    intrusive_ptr_container<RCObject> bs3;     // Recommended
    
    class SomeClass {
    private:
        Bear m_b1;                 // Triggers C.E.
        Bear* m_b2;                // Not allowed but no way to trigger C.E.
        intrusive_ptr<Bear> m_b3;  // Recommended
    };
    
  4. CLARIFIED: Declaring / returning raw pointers to RCObject (and subclasses) should be prohibited (Unfortunately, I don't think there exists a practical way to enforce it, i.e. triggering compilation errors when the users do not follow). See example source code in Item 3 above;

  5. Instances of RCObject subclasses should be cloneable just like Cloneable in Java. (MEC++1 Item 25);
  6. Users subclassing RCObject should be able to write "Factory Methods" for their subclasses. There should be no memory leak even if the returned value is ignored (not assigned to a variable). A mechanism which is close to this is autorelease in Objective-C;

    [ ADDED: cschwan and Kos pointed out that returning a "smart-pointer-to-RCObject" is sufficient to fulfill the requirement. ]

  7. CLARIFIED: Instances of RCObject subclasses should be able to be contained in an appropriate std:: or boost:: container. I mainly need a "std::vector-like" container, a "std::set-like" container and a "std::map-like" container. The baseline is that

    intrusive_ptr<RCObject> my_bear = v[10];
    

    and

    m["John"] = my_bear;
    

    work as expected;

  8. The source code should be compilable using a C++98 compiler with limited C++11 support (Visual Studio 2008 and gcc 4.6, to be exact).

更多信息

类定义

namespace zoo {
    class RCObject { ... };                  // Abstract
    class Animal : public RCObject { ... };  // Abstract
    class Bear : public Animal { ... };      // Concrete
    class Panda : public Bear { ... };       // Concrete
}

"非智能"版本 - createAnimal() [工厂方法]

zoo::Animal* createAnimal(bool isFacingExtinction, bool isBlackAndWhite) {
    // I wish I could call result->autorelease() at the end...
    zoo::Animal* result;

    if (isFacingExtinction) {
        if (isBlackAndWhite) {
            result = new Panda;
        } else {
            result = new Bear;
        }
    } else {
        result = 0;
    }

    return result;
}

"非智能"版本 - main()

int main() {
    // Part 1 - Construction
    zoo::RCObject* object1 = new zoo::Bear;
    zoo::RCObject* object2 = new zoo::Panda;
    zoo::Animal* animal1 = new zoo::Bear;
    zoo::Animal* animal2 = new zoo::Panda;
    zoo::Bear* bear1 = new zoo::Bear;
    zoo::Bear* bear2 = new zoo::Panda;
    //zoo::Panda* panda1 = new zoo::Bear;  // Should fail
    zoo::Panda* panda2 = new zoo::Panda;

    // Creating instances of RCObject on the stack should fail by following
    // the method described in the book MEC++1 Item 27.
    //
    //zoo::Bear b;                         // Should fail
    //zoo::Panda p;                        // Should fail

    // Part 2 - Object Assignment
    *object1 = *animal1;
    *object1 = *bear1;
    *object1 = *bear2;
    //*bear1 = *animal1;                   // Should fail

    // Part 3 - Cloning
    object1 = object2->clone();
    object1 = animal1->clone();
    object1 = animal2->clone();
    //bear1 = animal1->clone();            // Should fail

    return 0;
}

"智能"版本[未完成!]

/* TODO: How to write the Factory Method? What should be returned? */

#include <boost/intrusive_ptr.hpp>

int main() {
    // Part 1 - Construction
    boost::intrusive_ptr<zoo::RCObject> object1(new zoo::Bear);
    boost::intrusive_ptr<zoo::RCObject> object2(new zoo::Panda);
    /* ... Skip (similar statements) ... */
    //boost::intrusive_ptr<zoo::Panda> panda1(new zoo::Bear); // Should fail
    boost::intrusive_ptr<zoo::Panda> panda2(new zoo::Panda);

    // Creating instances of RCObject on the stack should fail by following
    // the method described in the book MEC++1 Item 27. Unfortunately, there
    // doesn't exist a way to ban the user from declaring a raw pointer to
    // RCObject (and subclasses), all it relies is self discipline...
    //
    //zoo::Bear b;                         // Should fail
    //zoo::Panda p;                        // Should fail
    //zoo::Bear* pb;                       // No way to ban this
    //zoo::Panda* pp;                      // No way to ban this

    // Part 2 - Object Assignment
    /* ... Skip (exactly the same as "non-smart") ... */

    // Part 3 - Cloning
    /* TODO: How to write this? */

    return 0;
}

上述代码(“智能版本”)显示了预期的使用模式。我不确定这种使用模式是否遵循最佳实践来使用智能指针。如果不是,请纠正我。

类似问题

参考资料

  • [EC++3]: 《Effective C++:改善程序与设计的55个具体做法》(第三版)作者:Scott Meyers
    • 条款7:在多态基类中将析构函数声明为虚函数

  • [MEC++1]: 《More Effective C++》:35种改进程序和设计的新方法(第一版),作者Scott Meyers
    • 条款25:虚拟构造函数和非成员函数
    • 条款27:要求或禁止基于堆的对象。

文章

  • [atomic]: boost::atomic的使用示例 - Boost.org

  • [ CP8394 ]: 智能指针,提高代码质量 - CodeProject
    • [ section ]: intrusive_ptr - 轻量级共享指针

  • [DrDobbs]: C++中用于具有侵入式引用计数的基类 - Dr. Dobb's

10
这是我见过的最常问的问题。 - sbooth
1
关于7:您不能直接将RCObject的实例“push_back”到“std ::”容器中,因为它们的大小可能不同。我猜您想存储指针或类似于“std :: shared_ptr <RCObject>”的东西。 - cschwan
1
关于6:只需让工厂函数返回一个智能指针。 - cschwan
1
那么,你尝试了什么?;-) - Yakk - Adam Nevraumont
1
@AsukaKenji-SiuChingPong-:如果将返回值赋值给变量,它将被复制,因此它会按预期工作。 - cschwan
显示剩余5条评论
2个回答

2

make_shared可以在相同的分配块中创建一个类实例和引用计数器。我不确定为什么您认为intrusive_ptr会有更好的性能:当已经存在无法删除的引用计数机制时,它非常棒,但在这种情况下并不是这样。

对于克隆,我会将其实现为一个自由函数,该函数接受智能指针并返回相同的智能指针。它是友元,并调用基类中的私有纯虚拟克隆方法,该方法返回指向基类的共享指针,然后进行快速智能指针转换以获得指向派生类的共享指针。如果您喜欢将克隆作为方法使用,则可以使用crtp来复制此内容(将私有克隆命名为secret_clone之类)。这为您提供了具有很少开销的协变智能指针返回类型。

具有一系列基类的Crtp通常需要同时传递基类和派生类。crtp类从基类派生,并具有通常的self(),该函数返回派生类。

工厂函数应返回智能指针。您可以使用自定义删除程序技巧来获取最后清理的预销毁方法调用。

如果您非常偏执,可以阻止大多数方法获取到类的原始指针或引用:在智能指针上阻止operator*。然后,获取原始类的唯一途径是显式调用operator->

另一种要考虑的方法是unique_ptr和对其的引用。您需要共享所有权和生命周期管理吗?它确实使某些问题更简单(共享所有权)。

请注意,悬空的弱指针会阻止内存从make_shared中回收。

始终使用智能指针的一个严重缺点是无法在堆栈实例或直接在容器内部拥有实例。这两个都可以成为严重的性能提升。


你说:“我不确定为什么你认为intrusive_ptr会有更好的性能。” 这部分是因为我对Boost不熟悉(我提到了它!:-))。我没有提到的另一个原因是,我将编写一个C++包装器来使用JSON-C库(该库已经具有引用计数),并且我将使RCObject成为JsonObject的父类。 我认为混合使用smart_ptrintrusive_ptr可能会让人感到困惑,不是吗? - Siu Ching Pong -Asuka Kenji-
(1)想了一会儿,似乎让RCObject作为JsonObject的父类是一个错误,因为这将复制引用计数字段。 不管怎样,我会找到解决办法。 (2)对于我来说,unique_ptr不合适,因为我的对象将被共享(可能由多个线程共享,但我想在本帖子中忽略多线程)。 (3)针对性能问题,我将使用一个专门针对在多个线程中分配小对象进行优化的内存分配库(类似于tcmalloc)。 我将使一些类成为POD,并为它们使用常规的std ::容器。 - Siu Ching Pong -Asuka Kenji-
你考虑过使用不同的JSON库吗?有相当多的选择。 - Sebastian Redl
@SebastianRedlпјҡжҳҜзҡ„пјҢдҪҶжҲ‘дјҡеқҡжҢҒдҪҝз”ЁPOSIXе’ҢOpenGLпјҲе®ғ们жҳҜC APIпјүпјҢеӣ дёәжҲ‘з”ҡиҮіжІЎжңүйҖүжӢ©гҖӮ - Siu Ching Pong -Asuka Kenji-

1
  1. 禁止在堆栈上创建RCObject子类的实例([MEC++1] [mec ++ 1]第27项);

你的理由是什么?MEC ++举了“对象能够自杀”的例子,在游戏框架的情况下可能有意义。是这种情况吗?

如果您坚持避免更简单的解决方法,使用足够智能的智能指针应该是可行的。

请注意,如果是这种情况,则还应禁止使用new [] 在堆栈上创建此类对象的数组-这也会防止删除单个对象。您还可能希望禁止将RCObjects用作子对象(其他类中的成员)。这意味着您禁止完全使用RCObject值,并让客户端代码通过智能指针处理它们。

  1. 应避免声明/返回指向RCObject(和子类)的原始指针(不幸的是,我认为不存在一种发出编译错误以强制执行它的方法);

那么你肯定会使用弱指针来表示“我对这个对象感兴趣,但我不会保持它的存活状态”。

  1. 用户可以为其子类编写 ["工厂方法"][factory_method]。即使返回值被忽略(未分配给变量),也不应该出现内存泄漏。

这种函数将返回一个临时智能指针对象,引用计数为1。如果此临时对象未用于初始化另一个对象(从而进一步增加引用计数),则它将清除该对象。您是安全的。

  1. RCObject 子类的实例应该能够包含在 std::boost:: 容器中(或适当的容器中)。我主要需要类似于 std::vectorstd::setstd::map 的东西;

这与(3)有些不同。如果您坚持要求对象必须在堆上单独创建并通过智能指针传递(而不是作为值),那么您还应该使用智能指针的容器。

由于性能考虑,我想使用[intrusive_ptr][intrusive_ptr]而不是[shared_ptr][shared_ptr],但我对两者甚至任何其他建议都持开放态度;
您是否过早优化?
另外,我认为使用侵入式指针会剥夺使用弱引用的可能性——正如我之前提到的那样。
我想知道RCObject是否应该私有继承[boost::noncopyable][noncopyable];
如果您禁止值类型变量并提供虚拟Clone,则可能不需要公共复制构造函数。您可以制作一个私有复制构造函数,并在定义Clone时使用它。

1
通过仅强制使用POD和intrusive_ptr<RCObject>,几乎没有泄漏内存的机会。 即使客户端不理解intrusive_ptr,他们也可以通过将所有派生自RCObject的内容包装在intrusive_ptr中来遵循该习惯用法。 此外,还可以考虑使用typedeftypedef intrusive_ptr<_RCObject> RCObject; - Siu Ching Pong -Asuka Kenji-
1
对于弱引用部分:不,我不想要任何原始指针和弱引用(我希望我的客户也能避免它们)。我想要的是彻底禁止使用原始指针(如果可能的话)。然而,这似乎是不可能的。 在intrusive_ptr <RCObject> bear1(new Bear)中,new Bear返回Bear*。一种方法是完全禁止构造函数,并只允许工厂方法返回intrusive_ptr <RCObjects>,但这可能对实现RCObject子类的客户来说过于不友好。 - Siu Ching Pong -Asuka Kenji-
1
(4)对于工厂方法部分,@cschwan给出了相同的建议。我同意当我第一眼看到它时,它是有效的。稍后我会尝试一下,以确保它可以在没有C++11中引入的“移动语义”/“右值引用”的情况下正常工作。(5)由于我不知道哪种容器是合适的,所以我只列出了我知道的那些(但可能不合适)。对于基线,我需要一个类似于std::vector的容器,一个类似于std::set的容器和一个类似于intrusive_ptr<Whatever>std::map容器。 - Siu Ching Pong -Asuka Kenji-
1
(B)在Java中,你会说Bear b = new Bear();;在我的引擎中,你会说intrusive_ptr<_Bear> b(new _Bear);。请注意这里和我原来的评论中的下划线。使用typedef,你可以说Bear b(new _Bear);。或者,像在boost::atomic的示例中使用的另一种typedef风格,你可以说Bear::ptr_t b(new Bear);。上面的第二个例子可能看起来很丑,但只是选择让你感觉舒适的名称的问题。那么typedef intrusive_ptr<Bear> BearPointer;怎么样? - Siu Ching Pong -Asuka Kenji-
1
(D) 对于发布:这是一个很好的问题!由于RCObject是整个引擎的低级构建块,它只能保证内存泄漏的几率更低。它不关心大局——这是各种高级“管理器”的工作。例如,如果调用了“切换场景”函数,则管理器应确保与该场景相关的所有资源的引用计数减少1(可能通过将currentScene智能指针设置为另一个场景来实现)。如果删除了先前的currentScene,则会将“-1”作为链式效应传播到其字段中。 - Siu Ching Pong -Asuka Kenji-
显示剩余10条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接