gpt4 book ai didi

c++ - 关于智能指针及其不可避免的非决定论的问题

转载 作者:IT老高 更新时间:2023-10-28 22:16:05 26 4
gpt4 key购买 nike

在过去的两年里,我一直在我的项目中广泛使用智能指针(准确地说是 boost::shared_ptr)。我理解并欣赏它们的好处,我通常非常喜欢它们。但是我使用它们越多,我就越想念 C++ 在内存管理和 RAII 方面的确定性行为,我似乎喜欢在编程语言中。智能指针简化了内存管理过程并提供了自动垃圾回收等功能,但问题是一般使用自动垃圾回收和智能指针在(去)初始化的顺序中特别引入了某种程度的不确定性。这种不确定性剥夺了程序员的控制权,并且正如我最近意识到的那样,使设计和开发 API 的工作成为可能,而 API 的使用在开发时事先并不完全了解,这非常耗时,因为必须仔细考虑所有使用模式和极端情况。

为了详细说明,我目前正在开发一个 API。此 API 的某些部分要求某些对象在其他对象之前初始化或在其他对象之后销毁。换句话说,(去)初始化的顺序有时很重要。举一个简单的例子,假设我们有一个名为 System 的类。系统提供一些基本功能(在我们的示例中记录)并通过智能指针保存许多子系统。

class System {
public:
boost::shared_ptr< Subsystem > GetSubsystem( unsigned int index ) {
assert( index < mSubsystems.size() );
return mSubsystems[ index ];
}

void LogMessage( const std::string& message ) {
std::cout << message << std::endl;
}

private:
typedef std::vector< boost::shared_ptr< Subsystem > > SubsystemList;
SubsystemList mSubsystems;
};

class Subsystem {
public:
Subsystem( System* pParentSystem )
: mpParentSystem( pParentSystem ) {
}

~Subsystem() {
pParentSubsystem->LogMessage( "Destroying..." );
// Destroy this subsystem: deallocate memory, release resource, etc.
}

/*
Other stuff here
*/

private:
System * pParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
};

正如您已经知道的那样,子系统仅在系统上下文中才有意义。但是这种设计中的子系统很容易超过其父系统。
int main() {
{
boost::shared_ptr< Subsystem > pSomeSubsystem;
{
boost::shared_ptr< System > pSystem( new System );
pSomeSubsystem = pSystem->GetSubsystem( /* some index */ );

} // Our System would go out of scope and be destroyed here, but the Subsystem that pSomeSubsystem points to will not be destroyed.

} // pSomeSubsystem would go out of scope here but wait a second, how are we going to log messages in Subsystem's destructor?! Its parent System is destroyed after all. BOOM!

return 0;
}

如果我们使用原始指针来保存子系统,那么当我们的系统出现故障时,我们就会破坏子系统,当然,pSomeSubsystem 将是一个悬空指针。

尽管保护客户端程序员不受他们自己的影响并不是 API 设计人员的工作,但让 API 易于正确使用而难以错误使用是一个好主意。所以我问你们。你怎么认为?我应该如何缓解这个问题?你会如何设计这样一个系统?

提前致谢,
乔希

最佳答案

问题总结
这个问题有两个相互矛盾的问题。

  • Subsystem的生命周期管理s,允许他们在正确的时间移除。
  • Subsystem的客户需要知道Subsystem他们使用的是有效的。

  • 处理 #1 System拥有 Subsystem s 并且应该用它自己的范围来管理他们的生命周期。使用 shared_ptr s 因为这特别有用,因为它简化了销毁,但是您不应该分发它们,因为那样您会失去您正在寻求的关于它们的解除分配的确定性。
    处理 #2
    这是需要解决的更有趣的问题。更详细地描述问题,您需要客户端接收一个行为类似于 Subsystem 的对象。而那 Subsystem (并且它的父级 System )存在,但在 Subsystem 之后表现得很好被摧毁。
    这可以通过 Proxy Pattern 的组合轻松解决。 , State PatternNull Object Pattern .虽然这似乎是一个有点复杂的解决方案,但“只有在复杂性的另一面才有简单性”。作为库/API 开发人员,我们必须加倍努力使我们的系统健壮。此外,我们希望我们的系统能够像用户期望的那样直观地运行,并在他们试图滥用它们时优雅地衰减。这个问题有很多解决方案,但是,这个应该让你到达所有重要的点,正如你和 Scott Meyers说是“好用易,误用难”。
    现在,我假设在现实中, System交易 Subsystem 的一些基类s,你从中衍生出各种不同的 Subsystem s。我在下面介绍它为 SubsystemBase .您需要介绍一个 代理对象, SubsystemProxy下面,实现了 SubsystemBase的接口(interface)通过将请求转发到它所代理的对象。 (从这个意义上说,它很像 Decorator Pattern 的特殊用途应用程序。)每个 Subsystem创建这些对象之一,它通过 shared_ptr 持有。 , 并在通过 GetProxy() 请求时返回,由父级 System 调用对象时 GetSubsystem()叫做。
    System超出范围,每个都是 Subsystem对象被破坏。在他们的析构函数中,他们调用 mProxy->Nullify() ,这会导致他们的 代理对象更改其 国家 .他们通过更改为指向 来做到这一点。空对象 ,它实现了 SubsystemBase界面,但什么都不做。
    使用 状态模式 这里允许客户端应用程序完全无视特定的 Subsystem存在。此外,它不需要检查指针或保留应该被销毁的实例。
    代理模式 允许客户端依赖于一个轻量级的对象,该对象完全封装了 API 内部工作的细节,并维护了一个恒定、统一的接口(interface)。
    空对象模式 允许 代理原版后的功能 Subsystem已被删除。
    示例代码
    我在这里放了一个粗略的伪代码质量示例,但我对此并不满意。我已经将它重写为我上面描述的一个精确的编译(我使用 g++)示例。为了让它工作,我不得不介绍一些其他的类,但它们的用途应该从它们的名字中清楚地看出。我雇用了 Singleton PatternNullSubsystem类,因为您只需要一个是有道理的。 ProxyableSubsystemBaseSubsystem 中完全抽象出代理行为,让它对这种行为一无所知。这是类的UML图:
    UML Diagram of Subsystem and System Hierarchy
    示例代码:
    #include <iostream>
    #include <string>
    #include <vector>

    #include <boost/shared_ptr.hpp>


    // Forward Declarations to allow friending
    class System;
    class ProxyableSubsystemBase;

    // Base defining the interface for Subsystems
    class SubsystemBase
    {
    public:
    // pure virtual functions
    virtual void DoSomething(void) = 0;
    virtual int GetSize(void) = 0;

    virtual ~SubsystemBase() {} // virtual destructor for base class
    };


    // Null Object Pattern: an object which implements the interface to do nothing.
    class NullSubsystem : public SubsystemBase
    {
    public:
    // implements pure virtual functions from SubsystemBase to do nothing.
    void DoSomething(void) { }
    int GetSize(void) { return -1; }

    // Singleton Pattern: We only ever need one NullSubsystem, so we'll enforce that
    static NullSubsystem *instance()
    {
    static NullSubsystem singletonInstance;
    return &singletonInstance;
    }

    private:
    NullSubsystem() {} // private constructor to inforce Singleton Pattern
    };


    // Proxy Pattern: An object that takes the place of another to provide better
    // control over the uses of that object
    class SubsystemProxy : public SubsystemBase
    {
    friend class ProxyableSubsystemBase;

    public:
    SubsystemProxy(SubsystemBase *ProxiedSubsystem)
    : mProxied(ProxiedSubsystem)
    {
    }

    // implements pure virtual functions from SubsystemBase to forward to mProxied
    void DoSomething(void) { mProxied->DoSomething(); }
    int GetSize(void) { return mProxied->GetSize(); }

    protected:
    // State Pattern: the initial state of the SubsystemProxy is to point to a
    // valid SubsytemBase, which is passed into the constructor. Calling Nullify()
    // causes a change in the internal state to point to a NullSubsystem, which allows
    // the proxy to still perform correctly, despite the Subsystem going out of scope.
    void Nullify()
    {
    mProxied=NullSubsystem::instance();
    }

    private:
    SubsystemBase *mProxied;
    };


    // A Base for real Subsystems to add the Proxying behavior
    class ProxyableSubsystemBase : public SubsystemBase
    {
    friend class System; // Allow system to call our GetProxy() method.

    public:
    ProxyableSubsystemBase()
    : mProxy(new SubsystemProxy(this)) // create our proxy object
    {
    }
    ~ProxyableSubsystemBase()
    {
    mProxy->Nullify(); // inform our proxy object we are going away
    }

    protected:
    boost::shared_ptr<SubsystemProxy> GetProxy() { return mProxy; }

    private:
    boost::shared_ptr<SubsystemProxy> mProxy;
    };


    // the managing system
    class System
    {
    public:
    typedef boost::shared_ptr< SubsystemProxy > SubsystemHandle;
    typedef boost::shared_ptr< ProxyableSubsystemBase > SubsystemPtr;

    SubsystemHandle GetSubsystem( unsigned int index )
    {
    assert( index < mSubsystems.size() );
    return mSubsystems[ index ]->GetProxy();
    }

    void LogMessage( const std::string& message )
    {
    std::cout << " <System>: " << message << std::endl;
    }

    int AddSubsystem( ProxyableSubsystemBase *pSubsystem )
    {
    LogMessage("Adding Subsystem:");
    mSubsystems.push_back(SubsystemPtr(pSubsystem));
    return mSubsystems.size()-1;
    }

    System()
    {
    LogMessage("System is constructing.");
    }

    ~System()
    {
    LogMessage("System is going out of scope.");
    }

    private:
    // have to hold base pointers
    typedef std::vector< boost::shared_ptr<ProxyableSubsystemBase> > SubsystemList;
    SubsystemList mSubsystems;
    };

    // the actual Subsystem
    class Subsystem : public ProxyableSubsystemBase
    {
    public:
    Subsystem( System* pParentSystem, const std::string ID )
    : mParentSystem( pParentSystem )
    , mID(ID)
    {
    mParentSystem->LogMessage( "Creating... "+mID );
    }

    ~Subsystem()
    {
    mParentSystem->LogMessage( "Destroying... "+mID );
    }

    // implements pure virtual functions from SubsystemBase
    void DoSomething(void) { mParentSystem->LogMessage( mID + " is DoingSomething (tm)."); }
    int GetSize(void) { return sizeof(Subsystem); }

    private:
    System * mParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
    std::string mID;
    };



    //////////////////////////////////////////////////////////////////
    // Actual Use Example
    int main(int argc, char* argv[])
    {

    std::cout << "main(): Creating Handles H1 and H2 for Subsystems. " << std::endl;
    System::SubsystemHandle H1;
    System::SubsystemHandle H2;

    std::cout << "-------------------------------------------" << std::endl;
    {
    std::cout << " main(): Begin scope for System." << std::endl;
    System mySystem;
    int FrankIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Frank"));
    int ErnestIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Ernest"));

    std::cout << " main(): Assigning Subsystems to H1 and H2." << std::endl;
    H1=mySystem.GetSubsystem(FrankIndex);
    H2=mySystem.GetSubsystem(ErnestIndex);


    std::cout << " main(): Doing something on H1 and H2." << std::endl;
    H1->DoSomething();
    H2->DoSomething();
    std::cout << " main(): Leaving scope for System." << std::endl;
    }
    std::cout << "-------------------------------------------" << std::endl;
    std::cout << "main(): Doing something on H1 and H2. (outside System Scope.) " << std::endl;
    H1->DoSomething();
    H2->DoSomething();
    std::cout << "main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object." << std::endl;

    return 0;
    }
    代码输出:
    main(): Creating Handles H1 and H2 for Subsystems.
    -------------------------------------------
    main(): Begin scope for System.
    <System>: System is constructing.
    <System>: Creating... Frank
    <System>: Adding Subsystem:
    <System>: Creating... Ernest
    <System>: Adding Subsystem:
    main(): Assigning Subsystems to H1 and H2.
    main(): Doing something on H1 and H2.
    <System>: Frank is DoingSomething (tm).
    <System>: Ernest is DoingSomething (tm).
    main(): Leaving scope for System.
    <System>: System is going out of scope.
    <System>: Destroying... Frank
    <System>: Destroying... Ernest
    -------------------------------------------
    main(): Doing something on H1 and H2. (outside System Scope.)
    main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object.
    其他想法:
  • 我在其中一本 Game Programming Gems 书籍中读到的一篇有趣的文章谈到了使用 Null 对象进行调试和开发。他们专门讨论使用空图形模型和纹理,例如棋盘纹理,使缺失的模型真正脱颖而出。通过更改 NullSubsystem 也可以在此处应用相同的方法。对于 ReportingSubsystem每当访问它时,它都会记录调用和可能的调用堆栈。这将允许您或您图书馆的客户根据超出范围的内容追踪他们所在的位置,而无需导致崩溃。
  • 我在@Arkadiy 的评论中提到他在 System 之间提出的循环依赖和 Subsystem有点不愉快。它可以通过 System 轻松解决。派生自 Subsystem 的接口(interface)取决于,Robert C Martin 的应用 Dependency Inversion Principle .更好的做法是隔离 Subsystem 的功能。需要他们的父级,为此编写一个接口(interface),然后在 System 中保留该接口(interface)的实现者并将其传递给 Subsystem s,它将通过 shared_ptr 保存它.例如,您可能有 LoggerInterface , 您的 Subsystem用于写入日志,则可以导出 CoutLoggerFileLogger从它,并在 System 中保留一个这样的实例.
    Eliminating the Circular Dependency
  • 关于c++ - 关于智能指针及其不可避免的非决定论的问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/400993/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com