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

Как описано в статье Создание интерфейсов API с помощью C++/WinRT, при создании объекта с типом реализации вам нужно использовать семейство вспомогательных приложений winrt::make.As explained in Author APIs with C++/WinRT, when you create an object of implementation type, you should use the winrt::make family of helpers to do so. В этом разделе подробно описывается компонент C++/WinRT 2.0. Вы можете использовать его для диагностики ошибки с прямым выделением объекта с типом реализации в стеке.This topic goes in-depth on a C++/WinRT 2.0 feature that helps you to diagnose the mistake of directly allocating an object of implementation type on the stack.

Такие ошибки могут приводить к непонятным сбоям или повреждениям данных, отладка которых потребует много усилий и времени.Such mistakes can turn into mysterious crashes or corruptions that are difficult and time-consuming to debug. То есть это важный компонент, и полезно понимать, как он работает.So this is an important feature, and it's worth understanding the background.

Подготовка — MyStringableSetting the scene, with MyStringable

Сначала рассмотрим простую реализацию IStringable.First, let's consider a simple implementation of IStringable.

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

Теперь представьте, что вам нужно вызвать функцию (из реализации), которая ожидает IStringable в виде аргумента.Now imagine that you need to call a function (from within your implementation) that expects an IStringable as an argument.

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

Проблема заключается в том, что наш тип MyStringable — это *не*IStringable.The trouble is that our MyStringable type is not an IStringable.

  • Наш тип MyStringable является реализацией интерфейса IStringable.Our MyStringable type is an implementation of the IStringable interface.
  • Тип IStringable — это проецируемый тип.The IStringable type is a projected type.

Важно!

Важно понимать различие между типом реализации и проецируемым типом.It's important to understand the distinction between an implementation type and a projected type. Чтобы ознакомиться с основными понятиями и терминами, обязательно прочитайте статьи Использование интерфейсов API с помощью C++/WinRT и Создание интерфейсов API с помощью C++/WinRT.For essential concepts and terms, be sure to read Consume APIs with C++/WinRT and Author APIs with C++/WinRT.

Различия между реализацией и проекцией на первый взгляд могут быть неочевидными.The space between an implementation and the projection can be subtle to grasp. Более того, для их сглаживания реализация предоставляет возможность неявного преобразования в каждый из проецируемых типов, которые она реализует.And in fact, to try to make the implementation feel a bit more like the projection, the implementation provides implicit conversions to each of the projected types that it implements. Не значит, что это просто сделать.That doesn't mean we can simply do this.

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

Ведь нам нужно получить ссылку, чтобы операторы преобразования можно было использовать в качестве кандидатов для разрешения вызова.Instead, we need to get a reference so that conversion operators may be used as candidates for resolving the call.

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

Так подойдет.That works. Неявное преобразование обеспечивает (очень эффективное) преобразование типа реализации в проецируемый тип, что очень удобно для множества сценариев.An implicit conversion provides a (very efficient) conversion from the implementation type to the projected type, and that's very convenient for many scenarios. Без такой возможности создание большинства типов реализации было бы непростой задачей.Without that facility, a lot of implementation types would prove very cumbersome to author. Если вы будете использовать только шаблон функции winrt::make (или winrt::make_self) для выделения реализации, у вас не возникнет проблем.Provided that you only use the winrt::make function template (or winrt::make_self) to allocate the implementation, then all is well.

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

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

Неявное преобразование все равно может создать определенные трудности.Still, implicit conversions can land you in trouble. Например, как в случае с этой бесполезной вспомогательной функцией.Consider this unhelpful helper function.

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

Или с этим, на первый взгляд, безобидным выражением.Or even just this apparently harmless statement.

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

К сожалению, компиляция такого кода выполнялась с использованием C++/WinRT 1.0 из-за наличия такого неявного преобразования.Unfortunately, code like that did compile with C++/WinRT 1.0, because of that implicit conversion. Проблема (и очень серьезная) заключается в том, что мы потенциально возвращаем проецируемый тип, указывающий на объект с подсчетом ссылок, резервная память которого находится во временном стеке.The (very serious) problem is that we're potentially returning a projected type that points to a reference-counted object whose backing memory is on the ephemeral stack.

Вот еще пример кода, скомпилированного с помощью C++/WinRT 1.0.Here's something else that compiled with C++/WinRT 1.0.

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

Необработанные указатели очень опасны и могут создавать множество ошибок.Raw pointers are dangerous and labor-intensive source of bugs. Не используйте их, если этого можно избежать.Don't use them if you don't need to. C++/WinRT позволяет выполнять операции эффективно без использования необработанных указателей.C++/WinRT goes out of its way to make everything efficient without ever forcing you into using raw pointers. Вот еще пример кода, скомпилированного с помощью C++/WinRT 1.0.Here's something else that compiled with C++/WinRT 1.0.

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

Это ошибка в нескольких аспектах.This is a mistake on several levels. У нас есть два разных счетчика ссылок для одного объекта.We have two different reference counts for the same object. Среда выполнения Windows (и классическая модель COM до нее) используют встроенный счетчик ссылок, который не совместим с std::shared_ptr.The Windows Runtime (and classic COM before it) is based on an intrinsic reference count that's not compatible with std::shared_ptr. std::shared_ptr, конечно же, имеет множество допустимых применений, но этот указатель совершенно излишен, если вы совместно используете объекты среды выполнения Windows (и классической модели COM).std::shared_ptr has, of course, many valid applications; but it's entirely unnecessary when you're sharing Windows Runtime (and classic COM) objects. Этот последний пример также скомпилирован с помощью C++/WinRT 1.0.Finally, this also compiled with C++/WinRT 1.0.

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

Такой подход также проблематичен.This is again rather questionable. Наличие уникального владельца находится в противоречии с общим временем существования встроенного счетчика ссылок MyStringable.The unique ownership is in opposition to the shared lifetime of the MyStringable's intrinsic reference count.

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

При использовании C++/WinRT 2.0 все такие попытки напрямую выделить типы реализации вызовут ошибку компилятора.With C++/WinRT 2.0, all of these attempts to directly allocate implementation types leads to a compiler error. Это лучшая из всех возможных ошибок, которая намного понятнее, чем загадочные ошибки среды выполнения.That's the best kind of error, and infinitely better than a mysterious runtime bug.

Если вам нужно выполнить реализацию, можете просто использовать winrt::make или winrt::make_self, как показано выше.Whenever you need to make an implementation, you can simply use winrt::make or winrt::make_self, as shown above. Только теперь, если забудете сделать это, вы получите ошибку компилятора с соответствующим напоминанием и ссылкой на абстрактную функцию с именем use_make_function_to_create_this_object.And now, if you forget to do so, then you'll be greeted with a compiler error alluding to this with a reference to an abstract function named use_make_function_to_create_this_object. Это не совсем static_assert, но около того.It's not exactly a static_assert; but it's close. Тем не менее, это самый надежный способ выявления всех описанных ошибок.Still, this is the most reliable way of detecting all of the mistakes described.

Это означает, что нам нужно внедрить несколько незначительных ограничений для реализации.It does mean that we need to place a few minor constraints on the implementation. Учитывая, что мы полагаемся на отсутствие переопределения для обнаружения прямого выделения, шаблон функции winrt::make должен каким-либо образом удовлетворить требования абстрактной виртуальной функции с переопределением.Given that we're relying on the absence of an override to detect direct allocation, the winrt::make function template must somehow satisfy the abstract virtual function with an override. Для этого он выполняет наследование от реализации с классом final, который предоставляет переопределение.It does so by deriving from the implementation with a final class that provides the override. Следует отметить несколько особенностей этого процесса.There are a few things to observe about this process.

Во-первых, виртуальная функция присутствует только в сборках для отладки.First, the virtual function is only present in debug builds. Это означает, что обнаружение не повлияет на размер виртуальной таблицы в ваших оптимизированных сборках.Which means that detection isn't going to affect the size of the vtable in your optimized builds.

Во-вторых, так как производный класс, используемый winrt::make, имеет свойство final, значит все операции развиртуализации, которые может проследить оптимизатор, будут выполнены даже в том случае, если вы ранее не пометили класс реализации как final.Second, since the derived class that winrt::make uses is final, it means that any devirtualization that the optimizer can possibly deduce will happen even if you previously chose not to mark your implementation class as final. Что уже хорошо.So that's an improvement. Противоположностью будет невозможность реализации иметь свойство final.The converse is that your implementation can't be final. Опять же, это несущественно, так как созданный экземпляр типа всегда будет иметь свойство final.Again, that's of no consequence because the instantiated type will always be final.

В-третьих, вы свободно можете помечать любые виртуальные функции в своей реализации как final.Third, nothing prevents you from marking any virtual functions in your implementation as final. Разумеется, C++/WinRT сильно отличается от классической модели COM и таких реализаций, как WRL, в которых все компоненты реализации обычно виртуализованы.Of course, C++/WinRT is very different from classic COM and implementations such as WRL, where everything about your implementation tends to be virtual. В C++/WinRT виртуальная диспетчеризация используется только с двоичным интерфейсом приложения (ABI) (который всегда имеет свойство final), а ваши методы реализации зависят от времени компиляции или статического полиморфизма.In C++/WinRT, the virtual dispatch is limited to the application binary interface (ABI) (which is always final), and your implementation methods rely on compile-time or static polymorphism. Это позволяет избежать ненужного полиморфизма среды выполнения, а также отказаться от использования каких-либо виртуальных функций в вашей реализации C++/WinRT.That avoids unnecessary runtime polymorphism, and also means that there's precious little reason for virtual functions in your C++/WinRT implementation. Что намного упрощает работу и встраивание кода.Which is a very good thing, and leads to far more predictable inlining.

В-четвертых, так как winrt::make внедряет производный класс, ваша реализация не может содержать частный деструктор.Fourth, since winrt::make injects a derived class, your implementation can't have a private destructor. Частные деструкторы часто использовались с классическими реализациями COM, так как, опять же, все компоненты были виртуальными. И при типичном прямом вызове необработанных указателей запросто можно было случайно вызвать delete, а не метод Release.Private destructors were popular with classic COM implementations because, again, everything was virtual, and it was common to deal directly with raw pointers and thus was easy to accidentally call delete instead of Release. В C++/WinRT вам нужно постараться, чтобы получить возможность прямого взаимодействия с необработанными указателями.C++/WinRT goes out of its way to make it hard for you to deal directly with raw pointers. И вам нужно приложить очень много усилий, чтобы получить в C++/WinRT необработанный указатель, для которого потенциально можно вызвать delete.And you'd have to really go out of your way to get a raw pointer in C++/WinRT that you could potentially call delete on. Благодаря семантике значений вы работаете со значениями и ссылками, и только в редких случаях — с указателями.Value semantics means that you're dealing with values and references; and rarely with pointers.

То есть C++/WinRT позволяет по-новому взглянуть на написание классического кода по модели COM.So, C++/WinRT challenges our preconceived notions of what it means to write classic COM code. И это логично, так как WinRT отличается от нее.And that's perfectly reasonable because WinRT is not classic COM. Классическая модель COM — это язык ассемблера среды выполнения Windows.Classic COM is the assembly language of the Windows Runtime. Использовать ее в повседневной работе и не нужно.It shouldn't be the code you write every day. C++/WinRT позволяет вам создавать код, который похож на современный C++ и сильно отличается от классической модели COM.Instead, C++/WinRT gets you to write code that's more like modern C++, and far less like classic COM.

Важные APIImportant APIs