Visual C++ 2015

Современный C++ приходит в Windows API

Кенни Керр

Продукты и технологии:

Visual C++ 2015, Windows Runtime

В статье рассматриваются:

  • современный C++;
  • стандартный C++;
  • Windows Runtime;
  • Component Object Model (COM).

Visual C++ 2015 — это кульминация колоссальных усилий группы C++ в реализации современного C++ для платформы Windows. По мере выхода последних нескольких выпусков Visual C++ постепенно достиг высокого уровня современного языка C++ и в совокупности с библиотечными средствами образует совершенно потрясающую среду разработки универсальных Windows-приложений и компонентов. Visual C++ 2015 опирается на заметные достижения в тех более ранних выпусках и предоставляет зрелый компилятор, который поддерживает большую часть C++11 и подмножество C++ 2015. Можно поспорить насчет уровня полноты реализации, но, как я полагаю, будет честным отметить, что сейчас компилятор поддерживает самые важные языковые средства, позволяющие современному C++ открыть новую эру в разработке библиотек для Windows. И это действительно ключевой момент. Когда компилятор поддерживает создание эффективных и изящных библиотек, разработчики могут приступить к написанию отличных приложений и компонентов.

Вместо скучного перечисления новых средств или поверхностной пробежки по новым возможностям я пошагово продемонстрирую разработку некоторого традиционно сложного кода, который теперь, если честно, довольно приятно писать благодаря зрелости компилятора Visual C++. Я намерен показать то, что является внутренней частью Windows и лежит и будет лежать в основе практически любого значимого текущего и будущего API.

Есть некая ирония в том, что C++ наконец стал достаточно современным для COM. Да, я говорю о Component Object Model, которая в течение многих лет является фундаментом большей части Windows API и по-прежнему используется в качестве основы для Windows Runtime. Хотя COM безусловно связана с C++ в плане исходной архитектуры и в ней много позаимствовано из C++ в отношении семантических соглашений и соглашений по двоичному коду, настоящей элегантности никогда не было. Тех частей C++, которые не были портируемыми в достаточной мере, например dynamic_cast, приходилось избегать в пользу портируемых решений, что делало реализации на C++ более трудными в разработке. За прошедшие годы предлагалось много решений, чтобы COM стала более удобоваримой для разработчиков на C++. По-видимому, самым амбициозным из них было языковое расширение C++/CX, выпущенное группой Visual C++. По иронии судьбы эти усилия в улучшении поддержки Standard C++ превзошли C++/CX и сделали языковые расширения бессмысленными.

Чтобы доказать это, я покажу, как реализовать IUnknown и IInspectable исключительно на современном C++. В этих интерфейсах нет ничего современного или привлекательного. IUnknown по-прежнему остается центральной абстракцией для таких именитых API, как DirectX. И эти интерфейсы — с IInspectable, производным от IUnknown, — находятся в самой сердцевине Windows Runtime. Я поясню, как реализовать их безо всяких языковых расширений, таблиц интерфейсов (interface tables) или других макросов, — только эффективный и элегантный C++ с уймой богатой информации о типах, позволяющей компилятору и разработчику заранее «договориться» о том, что нужно получить на выходе.

Основная трудность — придумать способ описания списка интерфейсов, которые намерен реализовать класс COM или Windows Runtime, и сделать так, чтобы это было удобно разработчику и понятно компилятору. Конкретнее, этот список типов должен быть доступен таким образом, чтобы компилятор мог запрашивать и даже перечислять интерфейсы. Если я смогу успешно решить эту задачу, мне удастся заставить компилятор сгенерировать код IUnknown-метода QueryInterface и дополнительно IInspectable-метод GetIids. Именно эти два метода и представляют самую сложную задачу. Традиционно решение удавалось создать только с применением языковых расширений, уродливых макросов или огромного количества трудного в сопровождении кода.

Реализации обоих методов требуют списка интерфейсов, которые намерен реализовать некий класс. Естественный выбор для описания такого списка типов — шаблон с переменным количеством аргументов (variadic template):

template <typename ... Interfaces>
class __declspec(novtable) Implements : public Interfaces ...
{
};

Расширенный атрибут novtable __declspec не дает любым конструкторам и деструкторам инициализировать vfptr в таких абстрактных классах, что зачастую приводит к значительному сокращению размера кода. Шаблон класса Implements включает пакет параметров шаблона, тем самым превращая его в шаблон с переменным количеством аргументов. Пакет параметров (parameter pack) — это параметр шаблона, принимающий любое число аргументов шаблона. Фокус в том, что пакеты параметров обычно используются для того, чтобы функции могли принимать любое количество аргументов, но в данном случае я описываю шаблон, аргументы которого будут запрошены только на этапе компиляции. Интерфейсы никогда не появятся в списке параметров функции.

Одно из применений этих аргументов уже очевидно. Пакет параметров раскрывается и образует список открытых базовых классов. Конечно, я по-прежнему отвечаю за реализацию этих виртуальных функций, но к этому моменту могу описать конкретный класс, который реализует любое количество интерфейсов:

class Hen : public Implements<IHen, IHen2>
{
};

Поскольку пакет параметров раскрывается, чтобы указать список базовых классов, он эквивалентен тому, что я мог бы написать самостоятельно:

class Hen : public IHen, public IHen2
{
};

Изящество такой структуризации шаблона класса Implements в том, что теперь можно вставить реализацию разнообразного стереотипного кода в этот шаблон, а разработчик класса Hen может использовать эту незаметную абстракцию и по большей части не обращать внимания на магию, стоящую за всем этим.

SПока все замечательно. Теперь я рассмотрю саму реализацию IUnknown. У меня должна быть возможность реализовать ее полностью внутри шаблона класса Implements, учитывая информацию о типах, которой сейчас располагает компилятор. IUnknown предоставляет два механизма, которые важны для COM-классов так же, как кислород и вода для людей. Первый и, возможно, более простой из них — учет ссылок, посредством которого COM-объекты отслеживают свои сроки существования. COM предписывает применять какую-либо встраиваемую форму учета ссылок, где каждый объект отвечает за управление своим сроком существования, исходя из знания того, сколько остается незакрытых ссылок. Это противоположно учету ссылок смарт-указателей, таких как шаблон класса shared_ptr в C++11, где объекту ничего не известно о том, кому он принадлежит. Вы могли бы привести доводы за и против этих двух подходов, но на практике COM-подход зачастую более эффективен, и это просто принцип работы COM, поэтому вам придется смириться с этим. Помимо всего прочего, вы, вероятно, согласитесь, что обертывать COM-интерфейс в shared_ptr — идея просто ужасная!

Начну с единственного недостатка (издержек периода выполнения), вводимого шаблоном класса Implements:

protected:
  unsigned long m_references = 1;
  Implements() noexcept = default;
  virtual ~Implements() noexcept
  {}

Конструктор по умолчанию сам по себе не дает издержек; он просто гарантирует, что полученный конструктор, инициализирующий счетчик ссылок, является защищенным, а не открытым. Защищаются и счетчик ссылок, и виртуальный деструктор. Делая счетчик ссылок доступным производным классам, вы разрешаете более сложную композицию классов. Большинство классов может просто игнорировать это, но заметьте, что я инициализирую счетчик ссылок значением 1. Это идет вразрез с популярным мнением о том, что изначально счетчик должен быть равен 0, поскольку никаких ссылок пока нет. Этот подход популяризировался ATL, и несомненно на его распространенность повлияла книга Дона Бокса (Don Box) «Essential COM», но он довольно проблематичен, что может подтвердить внимательное изучение исходного кода ATL. Начиная с предположения, что ссылкой сразу же завладеет вызвавший код или что она будет подключена к смарт-указателю, вы обеспечиваете куда менее подверженный ошибкам процесс конструирования.

Виртуальный деструктор крайне удобен в том плане, что позволяет шаблону класса Implements реализовать учет ссылок, не заставляя сам конкретный класс предоставлять эту реализацию. Другим вариантом было бы использование необычно повторяющегося шаблона (curiously recurring template pattern), чтобы обойтись без виртуальной функции. Обычно я предпочитаю именно такой подход, но он слегка усложняет абстракцию, а поскольку COM-класс по своей природе имеет виртуальную таблицу (vtable), в этом случае нет веских причин избегать виртуальной функции. Подготовив эти примитивы, остается лишь реализовать AddRef и Release в шаблоне класса Implements. Прежде всего метод AddRef может просто использовать встроенную InterlockedIncrement для увеличения значения счетчика ссылок:

virtual unsigned long __stdcall AddRef() noexcept override
{
  return InterlockedIncrement(&m_references);
}

По большей части этот код понятен и без пояснений. Не пытайтесь придумывать какую-то сложную схему, которая позволила бы вам заменять по условию встроенные функции InterlockedIncrement и InterlockedDecrement на C++-операторы приращения (++) и уменьшения (--). В ATL пытаются делать это ценой больших издержек и усложнения. Если ваша цель — эффективность, лучше потратьте свои силы на то, чтобы избегать туманных вызовов AddRef и Release. И здесь современный C++ вновь приходит на помощь с его поддержкой семантики перемещения и способности передачи владения ссылками без изменения счетчика ссылок. Теперь метод Release усложняется крайне незначительно:

virtual unsigned long __stdcall Release() noexcept override
{
  unsigned long const remaining = InterlockedDecrement(&m_references);
  if (0 == remaining)
  {
    delete this;
  }
  return remaining;
}

Счетчик ссылок уменьшается, и результат присваивается локальной переменной. Это важно, так как данный результат должен быть возвращен, но, если бы объект нужно было уничтожить, тогда было бы недопустимо ссылаться на переменную-член. Предполагая, что незакрытых ссылок нет, объект просто удаляется вызовом ранее упомянутого виртуального деструктора. И на этом учет ссылок реализован, а конкретный класс Hen остается столь же простым, как и раньше:

class Hen : public Implements<IHen, IHen2>
{
};

Теперь пора посмотреть на чудесный мир QueryInterface. Реализация этого метода, принадлежащего IUnknown, — упражнение нетривиальное. Я подробно описываю в своих учебных курсах на Pluralsight, и вы можете прочитать о многих причудливых и великолепных способах написания собственной реализации в книге Дона Бокса (Don Box) «Essential COM» (Addison-Wesley Professional, 1998). Но имейте в виду: хотя это прекрасная книга о COM, она основана на C++98 и никак не отражает современный C++. Для экономии места и времени я буду предполагать, что вы в какой-то мере знакомы с реализацией QueryInterface, и сосредоточусь на его реализации с помощью современного C++. Вот сам этот виртуальный метод:

virtual HRESULT __stdcall QueryInterface(
  GUID const & id, void ** object) noexcept override
{
}

Получив GUID, который идентифицирует конкретный интерфейс, QueryInterface должен определить, реализует ли объект нужный интерфейс. Если да, он должен увеличить счетчик ссылок для объекта, а затем возвратить указатель на интерфейс через выходной параметр. Если запрошенного интерфейса нет, возвращается null-указатель. Поэтому я начну со скелета этого метода:

*object = // каким-то образом находим интерфейс
if (nullptr == *object)
{
  return E_NOINTERFACE;
}
static_cast<::IUnknown *>(*object)->AddRef();
return S_OK;

Итак, сначала QueryInterface пытается как-то найти нужный интерфейс. Если этот интерфейс не поддерживается, обязательно возвращается код ошибки E_NOINTERFACE. Заметьте, как я уже позаботился о требовании очищать указатель на интерфейс при неудаче. Вы должны рассматривать QueryInterface во многом как бинарную операцию. Он либо находит интерфейс, либо нет. Не пытайтесь проявлять здесь свои творческие порывы. Хотя спецификация COM допускает некоторые ограниченные варианты, большинство потребителей будет просто предполагать, что интерфейс не поддерживается независимо от того, какой код ошибки вы могли бы вернуть. Любая ошибка в вашей реализации, несомненно, заставит вас понапрасну мучиться с отладкой. QueryInterface слишком фундаментален, чтобы забавляться с ним.

Наконец, AddRef снова вызывается по указателю на полученный интерфейс для поддержки некоторых редких, но вполне вероятных сценариев композиции классов. Они не поддерживаются шаблоном класса Implements явным образом, но я предпочел бы привести здесь хороший пример. Важно учитывать, что операции, связанные со счетчиком ссылок, специфичны для интерфейса, а не для объекта. Вы не можете просто вызвать AddRef или Release в любом интерфейсе, принадлежащем объекту. Вы должны соблюдать правила COM, а иначе вы рискуете ввести недопустимый код, который может рухнуть самым непредсказуемым образом.

Как же распознать, что запрошенный GUID представляет интерфейс, который будет реализован классом? Вот где я могу вернуть информацию о типах, которую собирает шаблон класса Implements через свой пакет параметров. Вспомните, что моя цель — дать возможность компилятору реализовать это за меня. Я хочу, чтобы конечный код был столь же эффективным, как если бы я писал его вручную, или даже еще эффективнее. Поэтому я выдаю этот запрос с набором шаблонов функций с переменным числом аргументов, т. е. шаблонов функций, которые сами включают пакеты параметров. Я начну с шаблона функции BaseQueryInterface:

virtual HRESULT __stdcall QueryInterface(
  GUID const & id, void ** object) noexcept override
{
  *object = BaseQueryInterface<Interfaces ...>(id);

BaseQueryInterface — это, по сути, проекция современного C++ на IUnknown QueryInterface. Вместо возврата HRESULT он напрямую возвращает указатель на интерфейс. О неудаче со всей очевидностью сообщает null-указатель. Он принимает единственный аргумент — GUID, идентифицирующий искомый интерфейс. Еще важнее, что я расширяю пакет параметров шаблона класса так, чтобы функция BaseQueryInterface могла начать процесс перечисления интерфейсов. Поначалу вы могли подумать, раз BaseQueryInterface является членом шаблона класса Implements, он может просто напрямую обращаться к этому списку, но мне нужно позволить этой функции извлекать первый интерфейс в списке:

template <typename First, typename ... Rest>
void * BaseQueryInterface(GUID const & id) noexcept
{
}

Тем самым BaseQueryInterface может идентифицировать первый интерфейс и оставить остальные для последующего поиска. Видите ли, в COM есть ряд специфических правил для поддержки идентификации объекта, которые QueryInterface должен реализовать или, по крайней мере, придерживаться. В частности, запросы к IUnknown должны всегда возвращать точно тот же указатель, чтобы клиент мог определить, ссылаются ли два указателя интерфейсов на один и тот же объект. Функция BaseQueryInterface является отличным местом для реализации некоторых из этих аксиом. Таким образом, я мог бы начать со сравнения запрошенного GUID с первым аргументом шаблона, представляющим первый интерфейс, который класс намерен реализовать. Если они не совпадают, я проверяю, не запрашивается ли IUnknown:

if (id == __uuidof(First) || id == __uuidof(::IUnknown))
{
  return static_cast<First *>(this);
}

Предполагая, что один из них совпадает, я просто возвращаю точный указатель на первый интерфейс. Static_cast гарантирует, что компилятор не станет обременять себя неоднозначностью нескольких интерфейсов на основе IUnknown. Такое приведение типа просто подстраивает указатель, чтобы найти правильное местонахождение vtable класса, а поскольку все виртуальные таблицы интерфейсов начинаются с трех методов IUnknown, это совершенно корректно.

Здесь же можно заодно добавить необязательную поддержку запросов к IInspectable. IInspectable — штука довольно странная. В каком-то смысле это IUnknown в Windows Runtime, поскольку каждый интерфейс Windows Runtime, проецируемый на языки вроде C# и JavaScript, должен прямо наследовать от IInspectable, а не от одного IUnknown. Увы, это необходимо, чтобы подстроиться под то, как Common Language Runtime реализует объекты и интерфейсы, а в ней это делается в противоположность тому, как работает C++, и традиционному определению COM. Кроме того, это влечет за собой некоторые довольно неприятные последствия для производительности, когда дело доходит до композиции объектов, но эта тема столь обширна, что я раскрою ее в одной из следующих статей.

Что касается QueryInterface, я должен просто обеспечить возможность запросов к IInspectable на случай реализации класса Windows Runtime, а не традиционного COM-класса. Хотя явные правила COM по IUnknown не применимы к IInspectable, последний можно интерпретировать здесь во многом так же. Но тут возникают две трудные проблемы. Во-первых, мне нужно распознавать, наследует ли любой из реализованных интерфейсов от IInspectable. И во-вторых, мне требуется тип одного из таких интерфейсов, чтобы я мог вернуть корректно подстроенный указатель на интерфейс, избежав неоднозначности. Если бы я мог предположить, что первый интерфейс в списке всегда будет основан на IInspectable, то просто обновил бы BaseQueryInterface следующим образом:

if (id == __uuidof(First) ||
  id == __uuidof(::IUnknown) ||
  (std::is_base_of<::IInspectable, First>::value &&
  id == __uuidof(::IInspectable)))
{
  return static_cast<First *>(this);
}

Заметьте, что я использую типаж типа (type trait) is_base_of из C++11, чтобы определить, является ли первый аргумент шаблона интерфейсом, производным от IInspectable. Это гарантирует исключение последующего сравнения компилятором, если вы реализуете традиционный COM-класс без поддержки Windows Runtime. Тем самым я могу бесшовно поддерживать как классы Windows Runtime, так и традиционные COM-классы без дополнительной синтаксической сложности для разработчиков компонентов и без лишних издержек периода выполнения. Но это оставляет потенциальную возможность для крайне трудноуловимой ошибки в случае, если вам понадобится первым перечислить интерфейс, отличный от IInspectable. Чтобы исключить вероятность этой ошибки, я должен заменить is_base_of чем-то, что позволяет сканировать весь список интерфейсов:

template <typename First, typename ... Rest>
constexpr bool IsInspectable() noexcept
{
  return std::is_base_of<::IInspectable, First>::value ||
    IsInspectable<Rest ...>();
}

IsInspectable по-прежнему полагается на типаж типа is_base_of, но теперь применяется к каждому интерфейсу до тех пор, пока не будет найдено совпадение. Если никаких интерфейсов, основанных на IInspectable, не найдено, завершающая функция попадает сюда:

template <int = 0>
constexpr bool IsInspectable() noexcept
{
  return false;
}

О любопытном безымянном аргументе по умолчанию я расскажу чуть позже. Предполагая, что IsInspectable возвращает true, я должен найти первый интерфейс на основе IInspectable:

template <int = 0>
void * FindInspectable() noexcept
{
  return nullptr;
}
template <typename First, typename ... Rest>
void * FindInspectable() noexcept
{
  // Как-то находим
}

Я могу снова опереться на типаж типа is_base_of, но на этот раз возвращаю реальный указатель на интерфейс, если совпадение найдено:

#pragma warning(push)
#pragma warning(disable:4127) //условное выражение - константа
if (std::is_base_of<::IInspectable, First>::value)
{
  return static_cast<First *>(this);
}
#pragma warning(pop)
return FindInspectable<Rest ...>();

Затем BaseQueryInterface может просто использовать IsInspectable наряду с FindInspectable для поддержки запросов к IInspectable:

if (IsInspectable<Interfaces ...>() && 
  id == __uuidof(::IInspectable))
{
  return FindInspectable<Interfaces ...>();
}

И вновь конкретный класс Hen выглядит так:

class Hen : public Implements<IHen, IHen2>
{
};

Шаблон класса Implements будет гарантировать, что компилятор сгенерирует наиболее эффективный код независимо от того, наследует IHen или IHen2 от IInspectable или просто от IUnknown (или какого-то другого интерфейса). Теперь я могу наконец реализовать рекурсивную часть QueryInterface, чтобы охватить любые дополнительные интерфейсы, такие как IHen2 в предыдущем примере. BaseQueryInterface завершается вызовом шаблона функции FindInterface:

template <typename First, typename ... Rest>
void * BaseQueryInterface(GUID const & id) noexcept
{
  if (id == __uuidof(First) || id == __uuidof(::IUnknown))
  {
    return static_cast<First *>(this);
  }
  if (IsInspectable<Interfaces ...>() && 
    id == __uuidof(::IInspectable))
  {
    return FindInspectable<Interfaces ...>();
  }
  return FindInterface<Rest ...>(id);
}

Я вызываю этот шаблон функции FindInterface во многом так же, как изначально вызывал BaseQueryInterface. В этом случае я передаю ему остальные интерфейсы. Конкретнее, я раскрываю пакет параметров так, чтобы он мог вновь идентифицировать первый интерфейс в остальном списке. Но это представляет проблему. Поскольку пакет параметров шаблона не раскрывается как аргументы функции, я могу в итоге оказаться в досадной ситуации, где язык не позволит мне выразить то, что я хочу. Но об этом чуть позже. Этот «рекурсивный» шаблон FindInterface с переменным числом аргументов выглядит так, как вы, вероятно, и ожидали:

template <typename First, typename ... Rest>
void * FindInterface(GUID const & id) noexcept
{
  if (id == __uuidof(First))
  {
    return static_cast<First *>(this);
  }
  return FindInterface<Rest ...>(id);
}

Он отделяет свой первый аргумент шаблона от остальных, возвращая подстроенный указатель на интерфейс, если совпадение найдено. В ином случае он вызывает сам себя до тех пор, пока список интерфейсов не будет исчерпан. Хотя я свободно ссылаюсь на this при рекурсии на этапе компиляции, важно отметить, что этот шаблон функции (и другие похожие примеры в шаблоне класса Implements) с технической точки зрения не являются рекурсивными даже в период компиляции. Каждый экземпляр шаблона функции вызывает отличающийся экземпляр этого шаблона. Например, FindInterface<IHen, IHen2> вызывает FindInterface<IHen2>, а тот — FindInterface<>. Для настоящей рекурсии FindInterface<IHen, IHen2> должен был бы вызывать FindInterface<IHen, IHen2>, чего на деле нет.

Тем не менее, учитывайте, что эта «рекурсия» происходит при компиляции и она подобна тому, как если бы вы писали все эти выражения if вручную, одно за другим. Но теперь я столкнулся с препятствием. Как завершается эта последовательность? Когда список аргументов шаблона пуст, конечно. Проблема в том, что в C++ уже определено, что означает пустой список параметров шаблона:

template <>
void * FindInterface(GUID const &) noexcept
{
  return nullptr;
}

Это почти правильно, но компилятор сообщит вам, что шаблон функции не доступен для этой специализации. И если я не предоставляю эту завершающую функцию, компилятору не удастся скомпилировать заключительный вызов, когда пакет параметров пуст. Это не тот случай, когда следует использовать перегрузку функции, потому что список аргументов остается тем же. К счастью, решение достаточно простое. Я могу отказаться от завершающей функции, выглядящей как специализация, предоставив безымянный аргумент по умолчанию:

template <int = 0>
void * FindInterface(GUID const &) noexcept
{
  return nullptr;
}

Компилятор счастлив, и, если будет запрошен неподдерживаемый интерфейс, эта завершающая функция просто вернет null-указатель, а виртуальный метод QueryInterface — код ошибки E_NOINTERFACE. И на этом с IUnknown все. Если вас интересует лишь традиционная COM, вы можете безопасно остановиться на этом, поскольку в таком случае у вас есть все, что когда-либо может понадобиться. Здесь стоит напомнить еще раз, что компилятор будет оптимизировать эту реализацию QueryInterface с ее разнообразными вызовами «рекурсивных» функций и выражений-констант (constant expressions), так что код будет, как минимум, не хуже того, что вы могли бы написать вручную. И того же можно достичь применительно к IInspectable.

В случае классов Windows Runtime появляется дополнительная сложность из-за необходимости реализации IInspectable. Этот интерфейс не столь фундаментален, как IUnknown, и предоставляет сомнительный набор средств в сравнении с абсолютно необходимыми функциями IUnknown. Однако дискуссию на эту тему я оставлю для одной из будущих статей, а здесь сосредоточусь на эффективной реализации с помощью современного C++, поддерживающей любой класс Windows Runtime. Сначала я разделаюсь с виртуальными функциями GetRuntimeClassName и GetTrustLevel. Оба эти метода сравнительно тривиальны в реализации и используются редко, поэтому в их реализации можно особо не вдаваться. Метод GetRuntimeClassName должен возвращать строку Windows Runtime с полным именем класса исполняющей среды, который представляется объектом. Я возложу реализацию на сам класс, если кто-то решит поступить именно так. Шаблон класса Implements может просто возвращать E_NOTIMPL, указывая, что данный метод не реализован:

HRESULT __stdcall GetRuntimeClassName(HSTRING * name) noexcept
{
  *name = nullptr;
  return E_NOTIMPL;
}

Аналогично метод GetTrustLevel просто возвращает константу из перечисления:

HRESULT __stdcall GetTrustLevel(TrustLevel * trustLevel) noexcept
{
  *trustLevel = BaseTrust;
  return S_OK;
}

Заметьте, что я не помечаю явным образом эти методы IInspectable как виртуальные функции. Отсутствие объявления с ключевым словом virtual позволяет компилятору удалять эти методы в случае, если COM-класс на самом деле не реализует никакие интерфейсы IInspectable. Теперь переключимся на метод GetIids в IInspectable. Он еще больше подвержен ошибкам, чем QueryInterface. Хотя его реализация не столь критична, эффективность реализации, генерируемой компилятором все же желательна. GetIids возвращает динамически создаваемый массив с GUID. Каждый GUID представляет один интерфейс, которые реализует объект. Поначалу вы могли подумать, что это просто объявление того, что объект поддерживает через QueryInterface, но это правильно лишь номинально. Метод GetIids может утаивать некоторые интерфейсы от публикации. Во всяком случае я начну с его базового определения:

HRESULT __stdcall GetIids(unsigned long * count, 
  GUID ** array) noexcept
{
  *count = 0;
  *array = nullptr;

Первый параметр указывает на передаваемую вызывающим кодом переменную, которой метод GetIids должен присвоить количество интерфейсов в конечном массиве. Второй — указывает на массив с GUID и то, как реализация передает динамически создаваемый массив обратно вызвавшему коду. Здесь я начал с очистки обоих параметров — просто для надежности. Теперь мне нужно определить, сколько интерфейсов реализует класс. Для этого просто используйте оператор sizeof, который умеет сообщать размер пакета параметров:

unsigned const size = sizeof ... (Interfaces);

Это очень удобно, и компилятор счастлив сообщить количество аргументов шаблона, которые присутствовали бы, если этот пакет параметров был бы раскрыт. Это также является фактически выражением-константой, создающим значение, известное при компиляции. Причина, по которой это не делается, как я уже упоминал, в том, что в реализациях GetIids весьма часто скрывают некоторые интерфейсы, которыми не желают ни с кем делиться. Такие интерфейсы называют скрытыми (cloaked interfaces). Любой может запросить их через QueryInterface, но GetIids не сообщит, что они доступны. Таким образом, мне нужно предоставить замену при компиляции для оператора sizeof с варьируемым количеством аргументов, которая исключает скрытые интерфейсы, и обеспечить какой-то способ объявлять и идентифицировать такие интерфейсы. Начнем с последнего. Я хочу предельно упростить это разработчикам компонентов при реализации классов. Я могу предоставить шаблон класса Cloaked для дополнения объявлений любых скрытых интерфейсов:

template <typename Interface>
struct Cloaked : Interface {};

Затем я могу реализовать специальный интерфейс IHenNative в конкретном классе Hen, который не должен быть известен всем потребителям:

class Hen : public Implements<IHen, IHen2, Cloaked<IHenNative>>
{
};

Поскольку шаблон класса Cloaked наследует от своего аргумента шаблона, существующая реализация QueryInterface по-прежнему будет без проблем работать. Я просто добавил немного дополнительной информации о типах, которую теперь могу запрашивать (тоже на этапе компиляции). Для этого я определю типаж типа IsCloaked, чтобы легко запрашивать любой интерфейс и определять, является ли он скрытым:

template <typename Interface>
struct IsCloaked : std::false_type {};
template <typename Interface>
struct IsCloaked<Cloaked<Interface>> : std::true_type {};

Далее можно подсчитать количество скрытых интерфейсов, вновь используя шаблон рекурсивной функции с переменным количеством аргументов:

template <typename First, typename ... Rest>
constexpr unsigned CounInterfaces() noexcept
{
  return !IsCloaked<First>::value + CounInterfaces<Rest ...>();
}

И, конечно, понадобится завершающая функция, которая просто возвращает нуль:

template <int = 0>
constexpr unsigned CounInterfaces() noexcept
{
  return 0;
}

Возможность выполнять такие арифметические вычисления при компиляции в современном C++ невероятна по своей мощи и потрясающе проста. Теперь можно продолжить обновление реализации GetIids, включив запрос этого счетчика:

unsigned const localCount = CounInterfaces<Interfaces ...>();

Одна шероховатость заключается в том, что поддержка компилятором выражений-констант еще не очень зрелая. Хотя это несомненно выражение-константа, компилятор пока не признает функции-члены constexpr. В идеале, я мог бы пометить шаблоны функции CountInterfaces как constexpr, и полученное выражение было бы выражением-константой, но компилятор еще не понимает такой вариант. С другой стороны, у меня нет сомнений в том, что у компилятора не возникнет никаких проблем с оптимизацией этого кода. Далее, если по какой-то причине CounInterfaces не находит скрытые интерфейсы, GetIids может просто вернуть код успеха, поскольку полученный массив будет пуст:

if (0 == localCount)
{
  return S_OK;
}

И вновь это, по сути, выражение-константа, а компилятор будет генерировать код без условной конструкции в любом случае. Другими словами, если скрытых интерфейсов нет, остальной код просто удаляется из реализации. В ином случае реализация вынуждена создавать подходящего размера массив с GUID, используя традиционный в COM механизм выделения памяти (allocator):

GUID * localArray = static_cast<GUID *>(CoTaskMemAlloc(sizeof(GUID) * localCount));

Конечно, это может закончиться неудачей, и тогда я просто возвращаю подходящий HRESULT:

if (nullptr == localArray)
{
  return E_OUTOFMEMORY;
}

К этому моменту у GetIids есть массив, готовый к заполнению GUID-идентификаторами. Как и следовало ожидать, мне нужно перечислить интерфейсы последний раз, чтобы скопировать в этот массив GUID каждого не скрытого интерфейса. Как и раньше, я буду использовать пару шаблонов функций:

template <int = 0>
void CopyInterfaces(GUID *) noexcept {}
template <typename First, typename ... Rest>
void CopyInterfaces(GUID * ids) noexcept
{
}

Шаблон с переменным количеством аргументов (вторая функция) может просто использовать типаж типа IsCloaked, чтобы определять, надо ли копировать GUID интерфейса, идентифицируемого его аргументом First шаблона, до приращения значения указателя. Тем самым массив проходится без необходимости отслеживать, сколько элементов он может содержать или по какому индексу массива следует вести запись. Я также подавляю предупреждение об этом выражении-константе:

#pragma warning(push)
#pragma warning(disable:4127) // условное выражение - константа
if (!IsCloaked<First>::value)
{
  *ids++ = __uuidof(First);
}
#pragma warning(pop)
CopyInterfaces<Rest ...>(ids);

Как видите, «рекурсивный» вызов CopyInterfaces в конце использует потенциальное увеличивающееся значение указателя. И я почти закончил. Реализацию GetIids можно завершить вызовом CopyInterfaces для заполнения массива перед возвратом вызвавшему:

CopyInterfaces<Interfaces ...>(localArray);
  *count = localCount;
  *array = localArray;
  return S_OK;
}

Тем временем, конкретный класс Hen остается в неведении обо всей работе, выполняемой компилятором в его интересах:

class Hen : public Implements<IHen, IHen2, Cloaked<IHenNative>>
{
};

И тут все именно так, как и должно быть в любой хорошей библиотеке. Компилятор Visual C++ 2015 обеспечивает невероятную поддержку Standard C++ на платформе Windows. Он позволяет разработчикам на C++ создавать элегантные и эффективные библиотеки. Вы получаете поддержку как разработки компонентов Windows Runtime на Standard C++, так и их использования из универсальных приложений Windows, написанных исключительно на Standard C++. Шаблон класса Implements — это всего один пример из современного C++ для Windows Runtime (см. moderncpp.com).


Кенни Керр (Kenny Kerr) — высококвалифицированный программист. Живет в Канаде. Автор учебных курсов для Pluralsight, обладатель звания Microsoft MVP. Ведет блог kennykerr.ca Кроме того, читайте его заметки в twitter.com/kennykerr.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Джеймсу Макнеллису (James McNellis).