次の方法で共有


実装タイプ用の拡張ポイント

winrt::implements 構造体テンプレートは、(ランタイム クラスとアクティブ化ファクトリの) 独自の C++/WinRT 実装が直接または間接的に派生するベースです。

このトピックでは、C++/WinRT 2.0 での winrt::implements の拡張ポイントについて説明します。 拡張ポイントを利用して実装タイプに適用することで、検査可能なオブジェクト(IInspectable インターフェイスにおける検査可能 の意味で)の既定の動作をカスタマイズできます。

これらの拡張ポイントを使用すると、実装型の破棄を延期したり、破棄中に安全にクエリを実行したり、投影されたメソッドにエントリをフックして終了したりすることができます。 このトピックでは、これらの機能について説明し、それらを使用するタイミングと方法について詳しく説明します。

遅延破棄

直接割り当ての診断 トピックでは、実装タイプにプライベートなデストラクタを持たせることはできません。

パブリックデストラクターを使用する利点は、遅延破棄を有効にできることです。これは、オブジェクトに対する最終的な IUnknown::Release 呼び出しを検出し、そのオブジェクトの所有権を取得してその破棄を無期限に延期できることです。

従来の COM オブジェクトは本質的に参照カウントされることを思い出してください。参照カウントは、 IUnknown::AddRef 関数と IUnknown::Release 関数を使用して管理されます。 Release の従来の実装では、参照カウントが 0 に達すると、従来の COM オブジェクトの C++ デストラクターが呼び出されます。

uint32_t WINRT_CALL Release() noexcept
{
    uint32_t const remaining{ subtract_reference() };
 
    if (remaining == 0)
    {
        delete this;
    }
 
    return remaining;
}

delete this;は、オブジェクトが占有するメモリを解放する前に、オブジェクトのデストラクターを呼び出します。 デストラクターで興味深い操作を行う必要がない場合は、これで十分に機能します。

using namespace winrt::Windows::Foundation;
... 
struct Sample : implements<Sample, IStringable>
{
    winrt::hstring ToString() const;
 
    ~Sample() noexcept
    {
        // Too late to do anything interesting.
    }
};

興味深いとはどういう意味ですか? 一つには、デストラクターは本質的に同期的です。 スレッドを切り替えることはできません。スレッド固有のリソースを別のコンテキストで破棄する場合があります。 特定のリソースを解放するために必要なその他のインターフェイスについては、オブジェクトに対して確実にクエリを実行することはできません。 リストは続きます。 破棄が簡単でない場合は、より柔軟なソリューションが必要です。 ここで C++/WinRT の final_release 関数が使用されます。

struct Sample : implements<Sample, IStringable>
{
    winrt::hstring ToString() const;
 
    static void final_release(std::unique_ptr<Sample> ptr) noexcept
    {
        // This is the first stop...
    }
 
    ~Sample() noexcept
    {
        // ...And this happens only when *unique_ptr* finally deletes the object.
    }
};

Release の C++/WinRT 実装を更新し、オブジェクトの参照カウントが 0 に遷移したときにfinal_releaseを呼び出しました。 その状態では、オブジェクトはそれ以上未処理の参照がないことを確信でき、それ自体の排他的所有権を持つようになりました。 そのため、それ自体の所有権を静的 final_release 関数に転送できます。

言い換えると、オブジェクトは、共有所有権をサポートするオブジェクトから、排他的に所有されたものに変換されています。 std::unique_ptrはオブジェクトの排他的な所有権を持っているので、std::unique_ptrがスコープ外になったとき(その前に他の場所に移動されない限り)、そのセマンティクスの一部としてオブジェクトが自然に破棄されます。そのため、パブリックデストラクターが必要になります。 それが鍵です。 std::unique_ptr がオブジェクトを維持している場合は、オブジェクトを無期限に使用できます。 オブジェクトを他の場所に移動する方法の図を次に示します。

struct Sample : implements<Sample, IStringable>
{
    winrt::hstring ToString() const;
 
    static void final_release(std::unique_ptr<Sample> ptr) noexcept
    {
        batch_cleanup.push_back(std::move(ptr));
    }
};

このコードは、アプリの実行時の将来の時点ですべてのオブジェクトをクリーンアップするジョブ の 1 つ batch_cleanupという名前のコレクションにオブジェクトを保存します。

通常、 std::unique_ptr が破棄されるとオブジェクトは破棄されますが、 std::unique_ptr::reset を呼び出すことによってその破棄を急ぐことができます。または、 std::unique_ptr をどこかに保存して延期することができます。

おそらく、より実用的かつ強力に、final_release 関数をコルーチンに変換することで、スレッドの中断や切り替えが必要な際に、それを可能にし、最終破棄を一箇所で処理することができます。

struct Sample : implements<Sample, IStringable>
{
    winrt::hstring ToString() const;
 
    static winrt::fire_and_forget final_release(std::unique_ptr<Sample> ptr) noexcept
    {
        co_await winrt::resume_background(); // Unwind the calling thread.
 
        // Safely perform complex teardown here.
    }
};

中断がポイントされると、呼び出し元のスレッド (最初は IUnknown::Release 関数の呼び出しを開始した) が返されるため、いったん保持したオブジェクトがそのインターフェイス ポインターを介して使用できなくなったことを呼び出し元に通知します。 多くの場合、UI フレームワークでは、オブジェクトを最初に作成した特定の UI スレッドでオブジェクトが破棄されるようにする必要があります。 この機能を使用すると、破棄がオブジェクトの解放から分離されるため、このような要件を満たすのは簡単です。

final_releaseに渡されるオブジェクトは単なる C++ オブジェクトであることに注意してください。COM オブジェクトではなくなりました。 たとえば、オブジェクトに対する既存の COM 弱参照は解決されなくなりました。

破棄処理中のセーフティクエリ

遅延破棄の概念に基づいて、破棄中にインターフェイスを安全に照会する仕組みがあります。

従来の COM は、2 つの主要な概念に基づいています。 1 つ目は参照カウントで、2 つ目はインターフェイスのクエリです。 AddRef および リリースに加えて、IUnknown インターフェイスは、QueryInterfaceを提供します。 このメソッドは、構成可能な型システムをシミュレートするときに XAML 階層を走査するために、XAML などの特定の UI フレームワークによって頻繁に使用されます。 簡単な例を考えてみましょう。

struct MainPage : PageT<MainPage>
{
    ~MainPage()
    {
        DataContext(nullptr);
    }
};

それは 無害 見えるかもしれません。 この XAML ページでは、デストラクター内のデータ コンテキストをクリアする必要があります。 ただし、 DataContextFrameworkElement 基底クラスのプロパティであり、個別の IFrameworkElement インターフェイスに存在します。 その結果、C++/WinRT は、DataContext プロパティを呼び出す前に、QueryInterface への呼び出しを挿入して正しい vtable を検索する必要があります。 しかし、デストラクターに入っている理由は、参照カウントが 0 に移行したということです。 ここで QueryInterface を 呼び出すと、その参照カウントが一時的にバンプされます。再び 0 に戻ると、オブジェクトは再び破棄されます。

これをサポートするために C++/WinRT 2.0 が強化されました。 簡略化された形式でのリリースの C++/WinRT 2.0 実装を次に示します。

uint32_t Release() noexcept
{
    uint32_t const remaining{ subtract_reference() };
 
    if (remaining == 0)
    {
        m_references = 1; // Debouncing!
        T::final_release(...);
    }
 
    return remaining;
}

予測したように、まず参照カウントをデクリメントし、未処理の参照がない場合にのみ作動します。 ただし、このトピックで前述した静的 final_release 関数を呼び出す前に、1 に設定することで参照カウントを安定させます。 これは、 のデバウンス (電気工学から用語を借用する) と呼ばれています。 これは、最終的な参照がリリースされないようにするために重要です。 その場合、参照カウントは不安定になり、 QueryInterface の呼び出しを確実にサポートすることはできません。

QueryInterface の呼び出しは、最終的な参照が解放された後は危険です。これは、参照カウントが無限に増加する可能性があるためです。 オブジェクトの寿命を延ばさない既知のコード パスのみを呼び出す必要があります。 C++/WinRT は、QueryInterface 呼び出し 確実に確実に実行できるようにすることで、途中でユーザーを満たします。

これは、参照カウントを安定化することによって行います。 最後の参照が解放されると、実際の参照カウントは 0 か、予期しない値のいずれかになります。 後者のケースは、弱い参照が関係している場合に発生する可能性があります。 いずれにしても、QueryInterface の後続の呼び出しが発生した場合、これは持続できません。なぜなら、その結果参照カウントが必ず一時的にインクリメントされるからです。そのため、デバウンスに関連しています。 これを 1 に設定すると、このオブジェクトで Release の最終呼び出しが再び行われることはありません。 std::unique_ptr がオブジェクトを所有しているため、QueryInterfaceと Release のペアへの制限された呼び出しが安全となり、これがまさに私たちが求めていることです。

もっと興味深い例を考えてみましょう。

struct MainPage : PageT<MainPage>
{
    ~MainPage()
    {
        DataContext(nullptr);
    }

    static winrt::fire_and_forget final_release(std::unique_ptr<MainPage> ptr)
    {
        co_await 5s;
        co_await winrt::resume_foreground(ptr->Dispatcher());
        ptr = nullptr;
    }
};

まず、 final_release 関数が呼び出され、クリーンアップのタイミングが実装に通知されます。 ここでは、final_release がコルーチンであることが判明しました。 最初の中断ポイントをシミュレートするには、まず、スレッド プールで数秒間待機します。 その後、ページのディスパッチャー スレッドで再開されます。 DispatcherDependencyObject 基底クラスのプロパティであるため、この最後の手順にはクエリが含まれます。 最後に、nullptrを割り当てることによって、ページが実際に削除されます。 その結果、ページのデストラクターが呼び出されます。

デストラクター内では、データ コンテキストをクリアします。これはわかっていますが、 FrameworkElement 基底クラスのクエリが必要です。

これはすべて、C++/WinRT 2.0 によって提供される参照カウントデバウンス (または参照カウント安定化) が原因で可能です。

メソッドの開始と終了フック

使用頻度が低い拡張ポイントとしては、abi_guard 構造体とabi_enter、および abi_exit 関数があります。

実装型で abi_enter関数が定義されている場合、その関数は、投影されたすべてのインターフェイス メソッドのエントリで呼び出されます ( IInspectable のメソッドはカウントされません)。

同様に、 abi_exitを定義すると、そのようなすべてのメソッドの終了時に呼び出されます。ただし、 abi_enter が例外をスローした場合は呼び出されません。 プロジェクションしたインターフェイス メソッド自体が例外をスローした場合でも、は引き続き呼び出されます

たとえば、abi_enter を使用して、ShutDown または Disconnect メソッド呼び出しの後で、オブジェクトが使用できない状態になったときにクライアントがそれを使おうとした場合、架空の invalid_state_error 例外をスローすることができます。 基になるコレクションが変更された場合、C++/WinRT 反復子クラスは、この機能を使用して 、abi_enter 関数で無効な状態例外をスローします。

単純な abi_enter 関数と abi_exit関数の上に、 abi_guardという名前の入れ子になった型を定義できます。 その場合、abi_guard のインスタンスは、投影されたインターフェイス メソッドの各 (非IInspectable) へのエントリに作成され、オブジェクトへの参照がコンストラクター パラメーターとして使用されます。 その後、 abi_guard はメソッドの終了時に破棄されます。 任意の追加の状態を abi_guard 型に配置できます。

独自の abi_guardを定義しない場合は、構築時に abi_enter を呼び出し、破棄時に abi_exit を呼び出す既定の abi_guard があります。

これらのガードは、 投影されたインターフェイスを介してメソッドが呼び出された場合にのみ使用されます。 実装オブジェクトでメソッドを直接呼び出すと、それらの呼び出しは、ガードなしで実装に直接移動します。

コード例を次に示します。

struct Sample : SampleT<Sample, IClosable>
{
    void abi_enter();
    void abi_exit();

    void Close();
};

void example1()
{
    auto sampleObj1{ winrt::make<Sample>() };
    sampleObj1.Close(); // Calls abi_enter and abi_exit.
}

void example2()
{
    auto sampleObj2{ winrt::make_self<Sample>() };
    sampleObj2->Close(); // Doesn't call abi_enter nor abi_exit.
}

// A guard is used only for the duration of the method call.
// If the method is a coroutine, then the guard applies only until
// the IAsyncXxx is returned; not until the coroutine completes.

IAsyncAction CloseAsync()
{
    // Guard is active here.
    DoWork();

    // Guard becomes inactive once DoOtherWorkAsync
    // returns an IAsyncAction.
    co_await DoOtherWorkAsync();

    // Guard is not active here.
}