Диагностика прямых выделений

Как описано в статье Создание интерфейсов API с помощью C++/WinRT, при создании объекта с типом реализации вам нужно использовать семейство вспомогательных приложений winrt::make. В этом разделе подробно описывается компонент C++/WinRT 2.0. Вы можете использовать его для диагностики ошибки с прямым выделением объекта с типом реализации в стеке.

Такие ошибки могут приводить к непонятным сбоям или повреждениям данных, отладка которых потребует много усилий и времени. То есть это важный компонент, и полезно понимать, как он работает.

Подготовка — MyStringable

Сначала рассмотрим простую реализацию IStringable.

struct MyStringable : implements<MyStringable, IStringable>
{
    winrt::hstring ToString() const { return L"MyStringable"; }
};

Теперь представьте, что вам нужно вызвать функцию (из реализации), которая ожидает IStringable в виде аргумента.

void Print(IStringable const& stringable)
{
    printf("%ls\n", stringable.ToString().c_str());
}

Проблема заключается в том, что наш тип MyStringable — это неIStringable.

  • Наш тип MyStringable является реализацией интерфейса IStringable.
  • Тип IStringable — это проецируемый тип.

Важно!

Важно понимать различие между типом реализации и проецируемым типом. Чтобы ознакомиться с основными понятиями и терминами, обязательно прочитайте статьи Использование интерфейсов API с помощью C++/WinRT и Создание интерфейсов API с помощью C++/WinRT.

Различия между реализацией и проекцией на первый взгляд могут быть неочевидными. Более того, для их сглаживания реализация предоставляет возможность неявного преобразования в каждый из проецируемых типов, которые она реализует. Не значит, что это просто сделать.

struct MyStringable : implements<MyStringable, IStringable>
{
    winrt::hstring ToString() const;
 
    void Call()
    {
        Print(this);
    }
};

Ведь нам нужно получить ссылку, чтобы операторы преобразования можно было использовать в качестве кандидатов для разрешения вызова.

void Call()
{
    Print(*this);
}

Так подойдет. Неявное преобразование обеспечивает (очень эффективное) преобразование типа реализации в проецируемый тип, что очень удобно для множества сценариев. Без такой возможности создание большинства типов реализации было бы непростой задачей. Если вы будете использовать только шаблон функции winrt::make (или winrt::make_self) для выделения реализации, у вас не возникнет проблем.

IStringable stringable{ winrt::make<MyStringable>() };

Возможные проблемы при использовании C++/WinRT 1.0

Неявное преобразование все равно может создать определенные трудности. Например, как в случае с этой бесполезной вспомогательной функцией.

IStringable MakeStringable()
{
    return MyStringable(); // Incorrect.
}

Или с этим, на первый взгляд, безобидным выражением.

IStringable stringable{ MyStringable() }; // Also incorrect.

К сожалению, компиляция такого кода выполнялась с использованием C++/WinRT 1.0 из-за наличия такого неявного преобразования. Проблема (и очень серьезная) заключается в том, что мы потенциально возвращаем проецируемый тип, указывающий на объект с подсчетом ссылок, резервная память которого находится во временном стеке.

Вот еще пример кода, скомпилированного с помощью C++/WinRT 1.0.

MyStringable* stringable{ new MyStringable() }; // Very inadvisable.

Необработанные указатели очень опасны и могут создавать множество ошибок. Не используйте их, если этого можно избежать. C++/WinRT позволяет выполнять операции эффективно без использования необработанных указателей. Вот еще пример кода, скомпилированного с помощью C++/WinRT 1.0.

auto stringable{ std::make_shared<MyStringable>(); } // Also very inadvisable.

Это ошибка в нескольких аспектах. У нас есть два разных счетчика ссылок для одного объекта. Среда выполнения Windows (и классическая модель COM до нее) используют встроенный счетчик ссылок, который не совместим с std::shared_ptr. std::shared_ptr, конечно же, имеет множество допустимых применений, но этот указатель совершенно излишен, если вы совместно используете объекты среды выполнения Windows (и классической модели COM). Этот последний пример также скомпилирован с помощью C++/WinRT 1.0.

auto stringable{ std::make_unique<MyStringable>() }; // Highly dubious.

Такой подход также проблематичен. Наличие уникального владельца находится в противоречии с общим временем существования встроенного счетчика ссылок MyStringable.

Решение при использовании C++/WinRT 2.0

При использовании C++/WinRT 2.0 все такие попытки напрямую выделить типы реализации вызовут ошибку компилятора. Это лучшая из всех возможных ошибок, которая намного понятнее, чем загадочные ошибки среды выполнения.

Если вам нужно выполнить реализацию, можете просто использовать winrt::make или winrt::make_self, как показано выше. Только теперь, если забудете сделать это, вы получите ошибку компилятора с соответствующим напоминанием и ссылкой на абстрактную функцию с именем use_make_function_to_create_this_object. Это не совсем static_assert, но около того. Тем не менее, это самый надежный способ выявления всех описанных ошибок.

Это означает, что нам нужно внедрить несколько незначительных ограничений для реализации. Учитывая, что мы полагаемся на отсутствие переопределения для обнаружения прямого выделения, шаблон функции winrt::make должен каким-либо образом удовлетворить требования абстрактной виртуальной функции с переопределением. Для этого он выполняет наследование от реализации с классом final, который предоставляет переопределение. Следует отметить несколько особенностей этого процесса.

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

Во-вторых, так как производный класс, используемый winrt::make, имеет свойство final, значит все операции развиртуализации, которые может проследить оптимизатор, будут выполнены даже в том случае, если вы ранее не пометили класс реализации как final. Что уже хорошо. Противоположностью будет невозможность реализации иметь свойство final. Опять же, это несущественно, так как созданный экземпляр типа всегда будет иметь свойство final.

В-третьих, вы свободно можете помечать любые виртуальные функции в своей реализации как final. Разумеется, C++/WinRT сильно отличается от классической модели COM и таких реализаций, как WRL, в которых все компоненты реализации обычно виртуализованы. В C++/WinRT виртуальная диспетчеризация используется только с двоичным интерфейсом приложения (ABI) (который всегда имеет свойство final), а ваши методы реализации зависят от времени компиляции или статического полиморфизма. Это позволяет избежать ненужного полиморфизма среды выполнения, а также отказаться от использования каких-либо виртуальных функций в вашей реализации C++/WinRT. Что намного упрощает работу и встраивание кода.

В-четвертых, так как winrt::make внедряет производный класс, ваша реализация не может содержать частный деструктор. Частные деструкторы часто использовались с классическими реализациями COM, так как, опять же, все компоненты были виртуальными. И при типичном прямом вызове необработанных указателей запросто можно было случайно вызвать delete, а не метод Release. В C++/WinRT вам нужно постараться, чтобы получить возможность прямого взаимодействия с необработанными указателями. И вам нужно приложить очень много усилий, чтобы получить в C++/WinRT необработанный указатель, для которого потенциально можно вызвать delete. Благодаря семантике значений вы работаете со значениями и ссылками, и только в редких случаях — с указателями.

То есть C++/WinRT позволяет по-новому взглянуть на написание классического кода по модели COM. И это логично, так как WinRT отличается от нее. Классическая модель COM — это язык ассемблера среды выполнения Windows. Использовать ее в повседневной работе и не нужно. C++/WinRT позволяет вам создавать код, который похож на современный C++ и сильно отличается от классической модели COM.

Важные API