Как инкапсулировать C API в классы RAII C ++? - PullRequest
7 голосов
/ 16 сентября 2010

Учитывая C API для библиотеки, управляющей сеансами, которая владеет элементами, каков наилучший дизайн для инкапсуляции C API в классы RAII C ++?

API C выглядит следующим образом:

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

Плюс другие функции, которые полезны для одного из этих типов (Session или Item) и отображаются непосредственно в функции-члены C ++ соответствующего объекта. Но они здесь не нужны. Мой основной интерес заключается в создании и уничтожении этих объектов с использованием RAII для управления правильным открытием и закрытием этих классов.

Моя первая идея для дизайна моих классов - pure и прямой 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 может быть разрушен (и вызвана функция CloseSession) до того, как все его объекты Item будут уничтожены. Это раздражает, потому что этого не должно было случиться. Даже если это ошибочное поведение возможно, а следовательно, и недопустимо при использовании 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;
            }
        }
};

Мне кажется, что это единственный способ убедиться, что сессия не закрыта до закрытия ее элементов: это отражает в дизайне, что объекты Item являются членами Сессия, и, следовательно, будет разрушена до того, как Сессия будет уничтожена.

Однако, это выглядит немного странно для меня, поскольку оставляет эти функции OpenItem и CloseItem в интерфейсе класса Session. Я искал что-то большее в строке RAII (для меня это означает использование конструктора для Item), но не могу представить способ инкапсулировать его, который бы обеспечил правильный порядок уничтожения.

Более того, использование указателей new и delete - это слишком много C ++ старого века. Должна быть возможность использовать вектор Item (вместо Item *) ценой правильного определения семантики перемещения для класса Item, но это будет ценой разрешения конструктора по умолчанию для Item, который будет создавать неинициализированный второй класс. Гражданин Предмет предметов.

Есть лучшие идеи дизайна?

Ответы [ 4 ]

5 голосов
/ 16 сентября 2010

Добавив еще один слой (и сделав свой RAII немного более явным), вы можете получить что-то довольно аккуратное.Конструкторы копирования по умолчанию и назначения для Сессий и Предметов делают правильную вещь.РУЧКА для сеанса будет закрыта после закрытия РУЧКИ для всех элементов.Нет необходимости держать в руках векторы детей, общие указатели отслеживают все это для вас ... Так что я думаю, что оно должно делать все, что вам нужно.

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;
};
2 голосов
/ 16 сентября 2010

Это интересная проблема, я думаю.

Прежде всего, для RAII вы обычно хотите реализовать конструктор копирования и оператор присваивания в целом, здесь 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 отвечает за управление временем жизни Item с, при этом фактический объект Item является не более чем прокси для дескриптора
  • У вас есть детерминированное время жизни ваших объектов: когда Session умирает, все элементы HANDLE эффективно закрываются

Тонкие проблемы: этот кодне безопасно в многопоточном приложении, но тогда я понятия не имею, нужно ли нам полностью сериализовать доступы к OpenItem и CloseItem, поскольку я не знаю, является ли базовая библиотека поточно-ориентированной.

Примечаниечто в этом проекте объект Session не может быть скопирован.Очевидно, что мы могли бы создать объект SessionManager (обычно одиночный, но не обязательный) и заставить его управлять Session таким же образом:)

1 голос
/ 11 декабря 2010

Чтобы развернуть комментарий 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);

HTH

0 голосов
/ 16 сентября 2010
...