C++ и Windows APIИсточник: oszone Кенни Керр
Windows API представляет проблему для разработчика на C++. Различные библиотеки, из которых состоит этот API, по большей части предоставляют либо функции и описатели в стиле C, либо интерфейсы в стиле COM. Ни то, ни другое совершенно неудобно для работы из C++ и требует некоторого уровня инкапсуляции или абстракции. Проблема для разработчика на C++ - определение уровня такой инкапсуляции. Разработчики, выросшие вместе с библиотеками вроде MFC и ATL, могут быть склонны к обертыванию всего и вся в классы и функции-члены, так как именно такой шаблон заложен в библиотеки C++, на которые они столь долго полагались. Другие разработчики высмеивают любую форму инкапсуляции и напрямую используют исходные функции, описатели и интерфейсы. Последних, пожалуй, вряд ли можно отнести к настоящим разработчикам на C++; это скорее программисты на C, у которых есть проблемы с самоидентификацией. Уверен, что для современных разработчиков на C++ более естественно нечто среднее. Поскольку я возобновляю свою рубрику в этом журнале, я покажу, как использовать C++0x, или C++ 2011 (это более вероятное название), в сочетании с Windows API, чтобы вывести из средневековья искусство программирования на основе "родных" интерфейсов Windows. На протяжении следующих нескольких месяцев я намерен предпринять расширенный экскурс по Windows Thread Pool API. Следуя за мной, вы откроете для себя, как писать удивительно масштабируемые приложения безо всяких причудливых новых языков и сложных или дорого обходящихся исполняющих сред. Все, что вам понадобится, - великолепный компилятор Visual C++, Windows API и желание учиться. Для удачного начала любой проект требует некоторой предварительной подготовки. Как же я собираюсь "обернуть" Windows API? Чтобы не тратить время на эти детали в каждой статье из этой рубрики, я поясню рекомендуемый мной подход в сегодняшней статье и в последующем буду просто опираться на него. Проблему интерфейсов в стиле COM я пока оставлю за скобками, так как в ближайших нескольких статьях они нам не понадобятся. Windows API состоит из многих библиотек, которые предоставляют набор функций в стиле C и один или несколько непрозрачных указателей, называемых описателями (handles). Эти описатели обычно представляют библиотеку или системный ресурс. Функции позволяют создавать ресурсы, манипулировать ими и освобождать, используя описатели. Например, функция CreateEvent создает объект события и возвращает описатель этого объекта. Чтобы освободить этот описатель и сообщить системе, что использование данного объекта события закончено, просто передайте описатель в функцию CloseHandle. Если других занятых описателей этого же объекта нет, система уничтожит его: auto h = CreateEvent( ... ); Новое в C++Если вы новичок в C++ 2011, то должен заметить, что ключевое слово auto сообщает компилятору логически определять тип переменной по выражению инициализации. Это полезно, когда вы не знаете тип выражения, как это часто бывает при метапрограммировании, или когда вы просто хотите меньше набирать текста. Но вы почти никогда не должны писать такой код. Несомненно, что самое ценное в C++ - концепция класса. Шаблоны впечатляют, Standard Template Library (STL) просто волшебна, но без класса ничто не имеет смысла в C++. Именно понятие класса делает программы на C++ лаконичными и надежными. Я не говорю о виртуальных функциях, наследовании и других замысловатых возможностях - только о конструкторе и деструкторе. Зачастую это все, что нужно, и знаете что? Классы не создают никаких издержек. На практике вы должны знать об издержках, связанных с обработкой исключений, и об этом мы поговорим в конце статьи. Чтобы приручить Windows API и сделать его доступным разработчикам на современном C++, нужен класс, инкапсулирующий описатель. Да, в вашей любимой библиотеке для C++, возможно, уже есть оболочка описателя, но годится ли она для C++ 2011? Сможете ли вы надежно хранить эти описатели в STL-контейнере и передавать их в своей программе, не теряя из виду, кому они принадлежат? Класс в C++ - отличная абстракция для описателей. Заметьте, что я не сказал "для объектов". Вспомните, что описатель - это представление объекта в вашей программе и чаще всего сам он не является объектом. Присмотра требует описатель - не объект. Иногда отношение "один к одному" между объектом Windows API и классом C++ очень удобно, но это отдельная история. Хотя описатели, как правило, непрозрачны, они, тем не менее, бывают разных типов и зачастую имеют небольшие семантические различия, которые требуют от шаблона класса адекватно, универсальным образом обертывать описатели. Параметры шаблона должны указывать тип описателя и конкретные характеристики, или особенности (traits) описателя. В C++ класс traits часто используется, чтобы предоставлять информацию о конкретном типе. Точно так же я могу написать единый шаблон класса для описателей и предоставлять разные классы traits для описателей разных типов в Windows API. Класс traits описателя также должен определять, как освобождается описатель, чтобы шаблон класса описателя мог автоматически освобождать его при необходимости. Вот класс traits для описателей события: struct handle_traits Поскольку такая семантика применяется во многих библиотеках Windows API, ее можно задействовать не только для объектов событий. Как видите, класс traits состоит только из статических функций-членов. В итоге компилятор может легко подставить код в строку, не внося никаких издержек, но обеспечивая высокую гибкость для метапрограммирования. Функция invalid возвращает значение недействительного описателя (invalid handle). Обычно это nullptr, новое ключевое слово в C++ 2011, представляющее значение null-указателя. В отличие от прежних альтернатив nullptr строго типизирован, чтобы нормально работать с шаблонами и механизмом перегрузки функций. Бывают случаи, где недействительный описатель определяется как нечто отличное от nullptr, и именно на такие случаи в класс traits включается функция invalid. Функция close инкапсулирует механизм, с помощью которого описатель закрывается или освобождается. Обрисовав контуры класса traits, двинемся дальше и приступим к определению шаблона класса описателя, как показано на рис. 1. Шаблон класса описателя
void close() throw() Type m_value; public: explicit unique_handle(Type value = Traits::invalid()) throw() : ~unique_handle() throw() Я назвал его unique_handle, потому что по духу он аналогичен стандартному шаблону класса unique_ptr. Многие библиотеки также используют идентичные типы и семантику описателей, поэтому имеет смысл предоставить typedef handle для наиболее распространенного случая: typedef unique_handle<HANDLE, handle_traits> handle; Теперь я могу создать объект события и его "описатель" таким образом: handle h(CreateEvent( ... )); Я объявил конструктор копии (copy constructor) и оператор присваивания копии (copy assignment operator) как закрытые и оставил их нереализованными. Это не дает компилятору автоматически генерировать их, так как автоматическая генерация редко годится для описателей. Windows API разрешает копировать определенные типы описателей, но эта концепция полностью отличается от семантики копирования в C++. Параметр value конструктора получает значение по умолчанию через класс traits. Деструктор вызывает закрытую функцию-член close, которая в свою очередь полагается на тот же класс в закрытии описателя, если это необходимо. Тем самым я получаю описатель, дружественный к стеку (stack-friendly) и безопасный при исключениях (exception-safe). Но я еще не закончил. Функция-член close полагается на наличие булева преобразования, чтобы определить, требует ли описатель закрытия. Хотя в C++ 2011 введены функции явного преобразования, в Visual C++ их пока нет, поэтому я использую универсальный подход к булеву преобразованию, чтобы избежать опасных неявных преобразований: struct boolean_struct { int member; }; bool operator==(unique_handle const &); public: operator boolean_type() const throw() Это означает, что теперь я могу просто проверять, действителен ли у меня описатель, не разрешая опасных неявных преобразований: unique_handle<SOCKET, socket_traits> socket; if (socket && event) {} // оба действительны? if (!event) {} // событие недействительно? int i = socket; // ошибка компилятора! if (socket == event) {} // ошибка компилятора! Использование более очевидного оператора bool привело бы к тому, что вы упустили бы последние две ошибки. Однако это все же позволяет сравнивать один сокет с другим - отсюда и возникает необходимость либо явно реализовать операторы проверки равенства, либо объявлять их закрытыми и оставлять нереализованными. Способ владения описателем у unique_handle аналогичен тому, как стандартный шаблон класса unique_ptr владеет объектом и управляет им через указатель. В таком случае имеет смысл предоставлять привычные функции-члены get, reset и release для управления нижележащим описателем. Функция get проста: Type get() const throw() Функция reset немного посложнее, но опирается на то, о чем я уже рассказывал: bool reset(Type value = Traits::invalid()) throw() Я позволил себе слегка изменить функцию reset из шаблона pattern, предоставляемого unique_ptr: моя версия возвращает bool-значение, указывающее, был ли объект сброшен в исходное состояние с действительным описателем. Это удобно при обработке ошибок, к которой я вскоре вернусь. Функция release теперь должна быть очевидной: Type release() throw()
|