直接割当ての診断Diagnosing direct allocations

C++/WinRT での API の作成」で説明されているように、実装型のオブジェクトを作成する場合は、ヘルパーの 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.

MyStringable を使用したシーンの設定Setting 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. 基本的な概念と用語については、「C++/WinRT での API の使用」と「C++/WinRT での API の作成」を参照してください。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.0 で発生する可能性のある落とし穴Potential 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. 同じオブジェクトに対して 2 つの異なる参照カウントがあります。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.0 を使用したソリューションThe 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.

第 1 に、仮想関数はデバッグ ビルド内にのみ存在します。First, the virtual function is only present in debug builds. つまり、検出は、最適化されたビルド内の vtable のサイズに影響しません。Which means that detection isn't going to affect the size of the vtable in your optimized builds.

第 2 に、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.

第 3 に、実装内の仮想関数を 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.

第 4 に、winrt::make によって派生クラスが挿入されるので、実装にプライベート デストラクターを含めることはできません。Fourth, since winrt::make injects a derived class, your implementation can't have a private destructor. プライベート デストラクターは、これもすべてが仮想であったため従来の COM 実装でよく使用されていました。また、生ポインターを直接処理するのが一般的だったで、Release の代わりに誤って delete を呼び出しやすくなっていました。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. また、ユーザーは delete を呼び出す可能性のある C++/WinRT 内の生ポインターの取得に "本当に" 尽力する必要があります。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 は従来の COM ではないため、これはまったく問題ありません。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