Как инкапсулировать C API в классы RAII C++?

Учитывая 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);

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

Вот почему мне интересно использовать следующий дизайн, в котором сеанс содержит свои элементы (это показывает фактическую связь) и является единственным классом, способным создавать и уничтожать элементы.

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;
            }
        }
};

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

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

Кроме того, использование указателей, new и delete — это слишком старый век С++. Должна быть возможность использовать вектор Item (вместо Item*) ценой правильного определения семантики перемещения для класса Item, но это будет ценой разрешения конструктора по умолчанию для Item, который создаст неинициализированный второй класс. Объекты Citizen Item.

Есть идеи получше?


person Didier Trosset    schedule 16.09.2010    source источник
comment
Если мы согласны с тем, что стандартная обертка RAII подходит, то возникает 2 вопроса: какой самый элегантный (RAII) способ открытия/закрытия элементов в сеансе и какая альтернатива вектору указателей, верно?   -  person stefaanv    schedule 16.09.2010
comment
В идеале я хотел бы иметь как элегантный способ открытия/закрытия элементов RAII, так и элементы, принадлежащие сеансам, чтобы они закрывались до начала сеанса. (вектор выдачи указателей не вопрос)   -  person Didier Trosset    schedule 16.09.2010
comment
Для меня два примера, которые вы показываете, служат разным целям, хотите ли вы работать с временными элементами (только в рамках, скажем, функции) или если вы хотите, чтобы элементы жили дольше, и в этом случае RAII не может вам помочь с очисткой элементов, пока сессия жива.   -  person stefaanv    schedule 16.09.2010
comment
@Didier: я хотел бы знать, есть ли риск столкновения идентификаторов. Если я дважды передам один и тот же идентификатор OpenItem, получу ли я тот же HANDLE? Если это так, то нам нужно управлять вызовом CloseItem с подсчетом ссылок, чтобы сделать это правильно, что, я думаю, налагает сортировку Factory как для Items, так и для Sessions.   -  person Matthieu M.    schedule 16.09.2010
comment
@Matthieu: если вы дважды передадите один и тот же идентификатор OpenItem, вы получите один и тот же дескриптор. Это не то, чего мне нужно избегать, поскольку библиотека имеет встроенный подсчет ссылок. Это означает, что при повторном открытии Item будет фактически закрыт только после второго вызова CloseItem. Это функция, с которой я могу жить, и я даже считаю это вольностью для пользователя библиотеки, которую я не хочу удалять.   -  person Didier Trosset    schedule 16.09.2010
comment
@Didier: как это аккуратно, значительно упрощает жизнь для упаковки, поскольку поэтому вам действительно не нужен подсчет ссылок! Тогда я отредактирую свой ответ.   -  person Matthieu M.    schedule 16.09.2010


Ответы (4)


Добавив еще один слой (и сделав свой RAII немного более явным), вы можете получить что-то довольно аккуратное. Конструкторы копирования по умолчанию и назначение для сеансов и элементов делают правильную вещь. 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;
};
person Michael Anderson    schedule 16.09.2010
comment
Если OpenItem и OpenSession могут возвращать NULL, вы можете либо изменить конструкторы, чтобы они вызывали в этом случае, либо использовать шаблон нулевого объекта. - person Michael Anderson; 16.09.2010
comment
Мне не нравится, что Session может протечь, если один из его Item где-то остался живым. Я предпочитаю детерминированное время жизни, когда это возможно, что облегчает отладку. - person Matthieu M.; 16.09.2010
comment
@Matthieu M. Как в вашем, так и в моем примере вам нужно быть осторожным в отношении времени жизни ваших объектов. В моем, если вы не будете осторожны, вы получите утечку, в вашем вы получите ошибку. Это дизайнерское решение, о котором пользователь должен знать в обоих случаях. (Сказав, что мне очень нравится ваш дизайн, +1) - person Michael Anderson; 16.09.2010
comment
Я согласен, что нужно принять сознательное решение, и я не отвергаю вашу идею сразу (во-первых, это намного проще). Просто отладив оба случая, я обнаружил, что ошибку анализировать гораздо проще, чем утечку, так как самой причиной утечки является непредвиденный доступ/копирование. Вот почему я стараюсь избегать shared_ptr, насколько это возможно, на самом деле я использовал их только здесь, потому что weak_ptr предлагает безопасный и простой способ узнать, жив ли объект. - person Matthieu M.; 16.09.2010

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

Прежде всего, для RAII вы обычно хотите реализовать конструктор копирования и оператор присваивания в целом, здесь HANDLE const помешал бы им, но вам действительно нужны объекты, которые нельзя копировать? И лучше сделать их безопасными для исключений.

Кроме того, есть проблема id: вы должны обеспечить уникальность или фреймворк сделает это за вас?

ИЗМЕНИТЬ:

Требования были уточнены с момента моего первого ответа, а именно:

  • в библиотеке уже реализован подсчет ссылок, нет необходимости заниматься этим самостоятельно

В этом случае у вас есть два варианта дизайна:

  • Используйте шаблон Observer: Item связывается обратно с Session, с которым он был создан, Session уведомляет его, когда он умирает (при использовании диспетчера сеансов это автоматизируется тем, что диспетчер сеансов владеет сеансом, а Item запрашивает менеджера о его сеанс)
  • Используйте схему @Michael, в которой Item совместно владеют объектом 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;
};

И класс сеанса

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

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

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

person Matthieu M.    schedule 16.09.2010
comment
Для RAII нет абсолютной необходимости в конструкторе копирования и операторе присваивания. Предоставления семантики перемещения в качестве конструктора перемещения и оператора перемещения достаточно. В любом случае, это не исключает существования Item класса NULL. - person Didier Trosset; 16.09.2010
comment
@Didier: нет, но я не видел этого требования в твоем вопросе. Однако он правильно аннулирует Item всякий раз, когда сеанс умирает. Если вы хотите, чтобы Item оставался действительным, рассмотрите ответ @Michael. Вам будет трудно контролировать время жизни Session, но вы больше не будете делать недействительными свои Item. - person Matthieu M.; 16.09.2010

Чтобы расширить комментарий STLSoft, используйте 1scoped__handle.html" rel="nofollow">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);

ХТН

person dcw    schedule 11.12.2010

Используйте shared_ptr: http://www.boost.org/doc/libs/1_44_0/libs/smart_ptr/sp_techniques.html#handle

person Abyx    schedule 16.09.2010