7

给定一个用于控制拥有项目的库的 C API,将 C API 封装到 RAII C++ 类中的最佳设计是什么?

C API 看起来像:

HANDLE OpenSession(STRING sessionID);
void CloseSession(HANDLE hSession);
HANDLE OpenItem(HANDLE hSession, STRING itemID);
void CloseItem(HANDLE hItem);

加上对这些类型(会话或项目)之一有用并直接映射到相关对象的 C++ 成员函数的其他函数。但这里不需要它们。我的主要兴趣是构建和销毁这些对象,使用 RAII 来管理这些类的正确打开和关闭。

我对课程设计的第一个想法是纯粹而直接的 RAII。包含的类接受一个容器对象作为构造函数参数。

class Session {
    HANDLE const m_hSession;
public:
    Session(STRING sessionID): m_hSession(OpenSession(sessionID)) {}
    ~Session() { CloseSession(m_hSession); }
};
class Item {
    HANDLE const m_hItem;
public:
    Item(HANDLE hSession, STRING itemID): m_hItem(OpenItem(hSession, itemID)) {}
    ~Item() { CloseItem(m_hItem); }
};

这种设计的缺点是允许不良行为:一个 Session 对象可以在其所有 Item 对象被破坏之前被破坏(并调用 CloseSession 函数)。这很烦人,因为它不应该发生。即使这种错误行为是可能的,因此是无效的,使用 C API,我希望在 C++ API 中通过设计来避免它。

这就是为什么我想知道使用以下设计,其中 Session 包含其项目(这显示了实际关系),并且是唯一能够构造和销毁项目的类。

class Item {
    HANDLE const m_hItem;
    Item(HANDLE hSession, STRING itemID): m_hItem(OpenItem(hSession, itemID) {}
    ~Item() { CloseItem(m_hItem); }
    friend class Session;
public:
};
class Session {
    HANDLE const m_hSession;
    typedef vector<Item *> VecItem;
    VecItem m_vecItem;
    Session(STRING sessionID): m_hSession(OpenSession(sessionID)) {}
    ~Session() {
        for (size_t n = 0 ; n < m_vecItem.size() ; ++n) delete m_vecItem[n];
        m_vecItem.clear();
        CloseSession(m_hSession);
        }
public:
    Item * OpenItem(STRING itemID) {
        Item *p = new Item(m_hSession, itemID);
        m_vecItem.push_back(p);
        return p;
        }
    void CloseItem(Item * item) {
        VecItem::iterator it = find(m_vecItem.begin(), m_vecItem.end(), item);
        if (it != m_vecItem.end()) {
            Item *p = *it; m_vecItem.erase(it); delete p;
            }
        }
};

在我看来,它是确保 Session 在其 Item 关闭之前不关闭的唯一方法:在设计中反映 Item 对象是 Session 的成员,因此将在 Session 被销毁之前被销毁。

但是,它看起来有点奇怪,因为它将这些函数 OpenItem 和 CloseItem 留在了 Session 类的接口中。我一直在寻找更多 RAII 行中的东西(对我来说,这意味着使用 Item 的构造函数),但无法想象一种封装它的方法,以确保正确的销毁顺序。

此外,使用指针、new 和 delete 太过古老的世纪 C++。应该可以使用 Item 的向量(而不是 Item*),代价是正确定义 Item 类的移动语义,但代价是允许 Item 的默认构造函数创建未初始化的第二类公民 Item 对象。

有更好的设计理念吗?

4

4 回答 4

5

通过添加另一层(并使您的 RAII 更加明确),您可以获得一些非常简洁的东西。Sessions 和 Items 的默认复制构造函数和赋值是正确的。会话的 HANDLE 将在所有项目的 HANDLE 关闭后关闭。没有必要保留孩子的向量,共享指针会为你跟踪所有这些......所以我认为它应该做你需要的一切。

class SessionHandle
{
   explicit SessionHandle( HANDLE in_h ) : h(in_h) {}
   HANDLE h;
   ~SessionHandle() { if(h) CloseSession(h); }
};

class ItemHandle
{
   explicit ItemHandle( HANDLE in_h ) : h(in_h) {}
   HANDLE h;
   ~ItemHandle() { if(h) CloseItem(h); }
};

class Session
{
   explicit Session( STRING sessionID ) : session_handle( OpenSession(sessionID) )
   {
   }
   shared_ptr<SessionHandle> session_handle;
};

class Item
{
   Item( Session & s, STRING itemID ) : 
     item_handle( OpenItem(s.session_handle.get(), itemID ) ), 
     session_handle( s.session_handle )
   {
   }
   shared_ptr<ItemHandle> item_handle;
   shared_ptr<SessionHandle> session_handle;
};
于 2010-09-16T10:59:17.940 回答
2

我认为这是一个有趣的问题。

首先,对于 RAII,您通常希望实现 Copy Constructor 和 Assignment Operator,这里HANDLE const会阻止它们,但是您真的想要无法复制的对象吗?最好也让它们异常安全。

此外,还有一个问题id:您必须确保唯一性还是框架会为您做到这一点?

编辑

自从我第一次回答以来,要求已经得到了精确化,即:

  • 该库已经实现了引用计数,无需自己处理

在这种情况下,您有两种设计方案:

  • 使用Observer模式:Item链接回Session创建它的对象Session,当它死亡时通知它(使用会话管理器,这是通过让会话管理器拥有会话并让Item管理器查询其会话来自动化的)
  • 使用@Michael 的方案,其中Items 共享对象的所有权Session,以便在至少一个仍然存在Session时不能被销毁。Item

我不太喜欢第二种解决方案,因为Session那时很难跟踪它的生命周期:你不能可靠地杀死它。

另一方面,正如您所说,第一个解决方案意味着存在空对象,这可能是不可接受的。

旧解决方案:

至于实际设计,我建议:

class Item
{
public:
  Item(): mHandle() {}

  Item(Session& session, std::string id): mHandle(session.CreateItem(id))
  {
  }

  void swap(Item& rhs)
  {
    using std::swap;
    swap(mHandle, rhs.mHandle);
  }

  void reset()
  {
    mHandle.reset();
  }

  /// Defensive Programming
  void do()
  {
    assert(mHandle.exists() && "do - no item");
    // do
  }

private:
  boost::weak_ptr<HANDLE const> mHandle;
};

和 Session 类

class Session
{
public:

private:
  typedef boost::weak_ptr<HANDLE const> weak_ptr;
  typedef boost::shared_ptr<HANDLE const> shared_ptr;
  typedef boost::unordered_map<std::string, shared_ptr> map_type;

  friend class Item;
  struct ItemDeleter
  {
    void operator()(HANDLE const* p) { CloseItem(*p); }
  };

  weak_ptr CreateItem(std::string const& id)
  {
    map_type::iterator it = mItems.find(id);
    if (it != mItems.end()) return it->second;

    shared_ptr p = shared_ptr(new OpenItem(mHandle, id), ItemDeleter());
    std::pair<map_type::iterator, bool> result =
      mItems(std::make_pair(id, p));

    return result.first->second;
  }

  map_type mItems;
  HANDLE const mHandle;
};

这传达了您要求的含义:

  • Session对象负责管理 s 的生命周期,Item实际Item对象只不过是句柄的代理
  • 你的对象有一个确定的生命周期:每当Session死亡时,所有的HANDLEto 项目都会有效地关闭

微妙的问题:这段代码在多线程应用程序中是不安全的,但是我不知道我们是否需要完全序列化访问OpenItemCloseItem因为我不知道底层库是否是线程安全的。

请注意,在此设计中,Session无法复制对象。显然我们可以创建一个SessionManager对象(通常是一个单例,但这不是必需的)并让他Session以相同的方式管理 s :)

于 2010-09-16T11:24:37.573 回答
1

要扩展STLSoft的评论,请使用 STLSoft 的scoped_handle智能指针,如下所示:

HANDLE hSession = OpenSession("session-X");
if(!hSession) {
 // Handle failure to open session
}
else {
  stlsoft::scoped_handle<HANDLE> session_release(hSession, CloseSession);

  HANDLE hItem = OpenItem(hSession, "item-Y");
  if(!hItem) {
     // Handle failure to open item
  }
  else {
      stlsoft::scoped_handle<HANDLE> item_release(hItem, CloseItem);

    // Use item
  }
}

如果“null”句柄值不为 0,则执行以下操作:

if(hSession != -1) {
 // Handle failure to open session
}
else {
  stlsoft::scoped_handle<HANDLE> session_release(hSession, CloseSession, -1);

高温高压

于 2010-12-11T06:45:21.643 回答
0

Use shared_ptr: http://www.boost.org/doc/libs/1_44_0/libs/smart_ptr/sp_techniques.html#handle

于 2010-09-16T11:15:56.760 回答