C++/WinRT の強参照と弱参照

Important

Windows アプリ SDKでビルドしますか? この記事のコードでは、UWP (Windows.UI.Xaml) 名前空間を使用します。 プロジェクトが WinUI 3 (Windows アプリ SDK) を対象とする場合は、Microsoft.UI.Xaml (および関連するMicrosoft.UI.*名前空間) を全体に置き換えます。 詳細については、完全なマッピングと UI 移行ガイドについては、Windows アプリ SDKへの UWP API のマッピングに関するページを参照してください。

Windows ランタイムは参照カウントシステムです。このようなシステムでは、強参照と弱参照 (および暗黙的なこのポインターなど) の重要性と区別について理解することが重要です。 このトピックで説明するように、これらの参照を正しく管理する方法を知ることは、スムーズに実行される信頼性の高いシステムと、予期しないクラッシュが発生するシステムの違いを意味する可能性があります。 C++/WinRT は、言語プロジェクションで手厚くサポートされたヘルパー関数を提供することで、より複雑なシステムを簡単かつ正確に構築できるよう開発者を支援します。

Note

ごく一部の例外を除き、C++/WinRT で使用または作成する Windows ランタイム 型では、弱参照のサポートは既定で有効になっています。 Windows。Ui。コンポジションWindows。Devices.Input.PenDevice は例外の例です。つまり、これらの型に対して弱参照のサポートが有効でない名前空間です。 自動 取り消しデリゲートが登録に失敗した場合も参照してください。

型を作成する場合は、このトピック の「C++/WinRT の弱い参照 」セクションを参照してください。

クラス メンバー コルーチンで この ポインターに安全にアクセスする

コルーチンとコード例の詳細については、「 C++/WinRT を使用したコンカレンシー操作と非同期操作」を参照してください。

次のコードリストは、クラスのメンバー関数であるコルーチンの一般的な例を示しています。 この例は、新しい Windows コンソール アプリケーション (C++/WinRT) プロジェクトの指定したファイルにコピーして貼り付けることができます。

// pch.h
#pragma once
#include <iostream>
#include <winrt/Windows.Foundation.h>

// main.cpp : Defines the entry point for the console application.
#include "pch.h"

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

struct MyClass : winrt::implements<MyClass, IInspectable>
{
    winrt::hstring m_value{ L"Hello, World!" };

    IAsyncOperation<winrt::hstring> RetrieveValueAsync()
    {
        co_await 5s;
        co_return m_value;
    }
};

int main()
{
    winrt::init_apartment();

    auto myclass_instance{ winrt::make_self<MyClass>() };
    auto async{ myclass_instance->RetrieveValueAsync() };

    winrt::hstring result{ async.get() };
    std::wcout << result.c_str() << std::endl;
}

MyClass::RetrieveValueAsync は作業に時間を費やし、最終的には MyClass::m_value データ メンバーのコピーを返します。 RetrieveValueAsync を呼び出すと、非同期オブジェクトが作成され、そのオブジェクトには暗黙的なこのポインターがあります (最終的には、m_valueがアクセスされます)。

コルーチンでは、最初の中断ポイントまで実行が同期され、制御が呼び出し元に返されることを覚えておいてください。 RetrieveValueAsync では、最初のco_awaitは最初の中断ポイントです。 コルーチンが再開されるまで (この場合は約 5 秒後)、にアクセスする暗黙的なm_valueポインターに何か起こった可能性があります。

イベントの完全なシーケンスを次に示します。

  1. main では、MyClass のインスタンスが作成されます (myclass_instance)。
  2. async オブジェクトが作成され、その this を介して myclass_instance を指しています。
  3. winrt::Windows::Foundation::IAsyncAction::get 関数は、最初の中断ポイントにヒットし、数秒間ブロックし、RetrieveValueAsync の結果を返します。
  4. RetrieveValueAsync は、 this->m_valueの値を返します。

手順 4 は、 これが 有効である限り安全です。

しかし、非同期操作が完了する前にクラス インスタンスが破棄された場合はどうでしょうか。 非同期メソッドが完了する前に、クラス インスタンスがスコープ外になる可能性があるさまざまな方法があります。 ただし、クラス インスタンスを nullptr に設定することで、それをシミュレートできます。

int main()
{
    winrt::init_apartment();

    auto myclass_instance{ winrt::make_self<MyClass>() };
    auto async{ myclass_instance->RetrieveValueAsync() };
    myclass_instance = nullptr; // Simulate the class instance going out of scope.

    winrt::hstring result{ async.get() }; // Behavior is now undefined; crashing is likely.
    std::wcout << result.c_str() << std::endl;
}

クラスインスタンスを破棄した時点以降は、もうそれを直接参照していないように見えます。 しかし、もちろん非同期オブジェクトには この ポインターがあり、それを使用してクラスインスタンス内に格納された値をコピーしようとします。 コルーチンはメンバー関数であり、this ポインターを何の問題もなく使用できることを前提としています。

このコードの変更により、クラス インスタンスが破棄され、無効になったため、手順 4 で 問題が発生 します。 非同期オブジェクトがクラス インスタンス内の変数にアクセスしようとすると、すぐにクラッシュします (または、完全に未定義の操作を行います)。

解決策は、非同期操作 (コルーチン) にクラス インスタンスへの独自の厳密な参照を提供することです。 現状のコードでは、コルーチンは実質的にクラス インスタンスへの生ポインター `this` を保持しています。しかし、それだけではクラス インスタンスの存続を保証するには不十分です。

クラス インスタンスを維持するには、 RetrieveValueAsync の実装を次のように変更します。

IAsyncOperation<winrt::hstring> RetrieveValueAsync()
{
    auto strong_this{ get_strong() }; // Keep *this* alive.
    co_await 5s;
    co_return m_value;
}

C++/WinRT クラスは、 winrt::implements テンプレートから直接または間接的に派生します。 そのため、C++/WinRT オブジェクトは、その protected メンバー関数 implements::get_strong を呼び出して、自身の this ポインターへの強参照を取得できます。 上記のコード例では、 strong_this 変数を実際に使用する必要はありません。 get_strong を呼び出すだけで、C++/WinRT オブジェクトの参照カウントがインクリメントされ、暗黙的な ポインターが 有効な状態が維持されます。

Important

get_strongwinrt::implements 構造体テンプレートのメンバー関数であるため、C++/WinRT クラスなどの winrt::implements から直接または間接的に派生するクラスからのみ呼び出すことができます。 winrt::implements からの派生の詳細と例については、「C++/WinRT を使用した API の作成」を参照してください。

これにより、手順 4 に進んだときに以前に発生した問題が解決されます。 クラス インスタンスへの他のすべての参照が消えた場合でも、コルーチンは依存関係が安定していることを保証する予防措置を講じます。

厳密な参照が適切でない場合は、代わりに implements::get_weak を呼び出して、 これに対する弱い参照を取得できます。 これにアクセスする前に、厳密な参照を取得できることを確認してください。 ここでも、 get_weakwinrt::implements 構造体テンプレートのメンバー関数です。

IAsyncOperation<winrt::hstring> RetrieveValueAsync()
{
    auto weak_this{ get_weak() }; // Maybe keep *this* alive.

    co_await 5s;

    if (auto strong_this{ weak_this.get() })
    {
        co_return m_value;
    }
    else
    {
        co_return L"";
    }
}

上の例では、弱参照は、厳密な参照が残っていないときにクラス インスタンスが破棄されないようにしません。 ただし、メンバー変数にアクセスする前に、厳密な参照を取得できるかどうかを確認する方法が提供されます。

イベント処理デリゲートを使用して この ポインターに安全にアクセスする

シナリオ

イベント処理に関する一般的な情報については、「 C++/WinRT でデリゲートを使用してイベントを処理する」を参照してください。

前のセクションでは、コルーチンとコンカレンシーの領域における潜在的な有効期間の問題を強調しました。 ただし、オブジェクトのメンバー関数を使用してイベントを処理する場合、またはオブジェクトのメンバー関数内のラムダ関数内からイベントを処理する場合は、イベント受信者 (イベントを処理するオブジェクト) とイベント ソース (イベントを発生させるオブジェクト) の相対的な有効期間を考慮する必要があります。 いくつかのコード例を見てみましょう。

次のコードリストでは、最初に単純な EventSource クラスを定義します。このクラスでは、追加されたすべてのデリゲートによって処理される汎用イベントが発生します。 このイベント例では、Windows::Foundation::EventHandler デリゲート型が使用されますが、ここでの問題と解決策は、すべてのデリゲート型に適用されます。

次に、 EventRecipient クラスは、ラムダ関数の形式で EventSource::Event イベントのハンドラーを提供します。

// pch.h
#pragma once
#include <iostream>
#include <winrt/Windows.Foundation.h>

// main.cpp : Defines the entry point for the console application.
#include "pch.h"

using namespace winrt;
using namespace Windows::Foundation;

struct EventSource
{
    winrt::event<EventHandler<int>> m_event;

    void Event(EventHandler<int> const& handler)
    {
        m_event.add(handler);
    }

    void RaiseEvent()
    {
        m_event(nullptr, 0);
    }
};

struct EventRecipient : winrt::implements<EventRecipient, IInspectable>
{
    winrt::hstring m_value{ L"Hello, World!" };

    void Register(EventSource& event_source)
    {
        event_source.Event([&](auto&& ...)
        {
            std::wcout << m_value.c_str() << std::endl;
        });
    }
};

int main()
{
    winrt::init_apartment();

    EventSource event_source;
    auto event_recipient{ winrt::make_self<EventRecipient>() };
    event_recipient->Register(event_source);
    event_source.RaiseEvent();
}

パターンは、イベント受信者 がこのポインターに 依存関係を持つラムダ イベント ハンドラーを持っているということです。 イベントの受信者がイベント ソースより長く存続する場合、その依存関係よりも長く存続することになります。 また、一般的なケースでは、パターンは適切に動作します。 UI ページがページ上にあるコントロールによって発生したイベントを処理する場合など、これらのケースの一部は明白です。 ページはボタンより長く存続するので、ハンドラーもボタンより長く存続します。 これは、受信者がソースを所有している場合 (データ メンバーなど)、または受信者とソースが兄弟であり、他のオブジェクトによって直接所有されている場合は、いつでも当てはまります。

ハンドラーが、それが依存している this より長く存続しないケースだと確信できる場合は、strong または weak なライフタイムを考慮することなく、this を通常どおりキャプチャできます。

ただし、this がハンドラー内での使用中より長く存続しないケースは依然として存在します(これには、非同期アクションや操作によって発生する完了イベントや進行状況イベントのハンドラーも含まれます)。そのため、そうしたケースへの対処方法を理解しておくことが重要です。

  • イベント ソースが イベントを同期的に発生させる場合は、ハンドラーを取り消して、それ以上イベントを受信しないことを確認できます。 ただし、非同期イベントの場合は、取り消した後 (特にデストラクター内で取り消す場合) でも、破棄が開始された後に、インフライト イベントがオブジェクトに到達する可能性があります。 破棄前に登録を解除する場所を見つけると、問題が軽減される可能性がありますが、堅牢なソリューションについては引き続きお読みください。
  • 非同期メソッドを実装するためにコルーチンを作成している場合は、可能です。
  • まれに、特定の XAML UI フレームワーク オブジェクト (SwapChainPanel など) では、受信者がイベント ソースから登録を解除せずに終了する可能性があります。

問題

この次のバージョンの main 関数は、イベント ソースがまだイベントを発生させている間に、イベント受信者が破棄された場合 (おそらくスコープ外になる) に何が起こるかをシミュレートします。

int main()
{
    winrt::init_apartment();

    EventSource event_source;
    auto event_recipient{ winrt::make_self<EventRecipient>() };
    event_recipient->Register(event_source);
    event_recipient = nullptr; // Simulate the event recipient going out of scope.
    event_source.RaiseEvent(); // Behavior is now undefined within the lambda event handler; crashing is likely.
}

イベント受信者は破棄されますが、その中のラムダ イベント ハンドラーは引き続き イベント イベントにサブスクライブされます。 そのイベントが発生すると、ラムダはその時点では無効な this ポインターを逆参照しようとします。 そのため、アクセス違反は、ハンドラー内のコード、またはコルーチンの継続処理がそれを使用しようとした結果として発生します。

Important

このような状況が発生した場合は、 この オブジェクトの有効期間について考える必要があります。キャプチャされた この オブジェクトがキャプチャを上回るかどうか。 そうでない場合は、次に示すように、強い参照または弱い参照でキャプチャします。

または、シナリオにとって意味があり、スレッドの考慮事項によってそれが可能になる場合は、受信者がイベントを処理した後、または受信者のデストラクターでハンドラーを取り消す方法もあります。 登録済みデリゲートの取り消しを参照してください。

ハンドラーを登録する方法は次のようになります。

event_source.Event([&](auto&& ...)
{
    std::wcout << m_value.c_str() << std::endl;
});

ラムダは、参照によってローカル変数を自動的にキャプチャします。 したがって、この例では、これと同等にこれを記述できました。

event_source.Event([this](auto&& ...)
{
    std::wcout << m_value.c_str() << std::endl;
});

どちらの場合も、 この ポインターを生でキャプチャするだけです。 これは参照カウントには影響しないため、現在のオブジェクトが破棄されるのを妨げるものは何もありません。

解決策

解決策は、強参照をキャプチャすることです(あるいは、後で見るように、そのほうが適切であれば弱参照をキャプチャします)。 強参照参照カウントを増やし、実際に現在のオブジェクトを存続させます。 キャプチャ変数 (この例では strong_this と呼ばれます) を宣言し、実装する呼び出しで初期化するだけです ::get_strongこれにより、この ポインターへの厳密な参照が取得されます。

Important

get_strongwinrt::implements 構造体テンプレートのメンバー関数であるため、C++/WinRT クラスなどの winrt::implements から直接または間接的に派生するクラスからのみ呼び出すことができます。 winrt::implements からの派生の詳細と例については、「C++/WinRT を使用した API の作成」を参照してください。

event_source.Event([this, strong_this { get_strong()}](auto&& ...)
{
    std::wcout << m_value.c_str() << std::endl;
});

現在のオブジェクトの自動キャプチャを省略し、暗黙的な これを使用する代わりにキャプチャ変数を使用してデータ メンバーにアクセスすることもできます。

event_source.Event([strong_this { get_strong()}](auto&& ...)
{
    std::wcout << strong_this->m_value.c_str() << std::endl;
});

厳密な参照が適切でない場合は、代わりに implements::get_weak を呼び出して、 これに対する弱い参照を取得できます。 弱参照 では、 現在のオブジェクトは維持されません。 したがって、メンバーにアクセスする前に、弱い参照から強力な参照を取得できることを確認してください。

event_source.Event([weak_this{ get_weak() }](auto&& ...)
{
    if (auto strong_this{ weak_this.get() })
    {
        std::wcout << strong_this->m_value.c_str() << std::endl;
    }
});

生のポインターをキャプチャする場合は、ポイント先のオブジェクトを有効にしておく必要があります。

メンバー関数をデリゲートとして使用する場合

ラムダ関数と同様に、これらの原則は、デリゲートとしてメンバー関数を使用する場合にも適用されます。 構文が異なるので、いくつかのコードを見てみましょう。 まず、 この ポインターを生で使用して、安全でない可能性のあるメンバー関数イベント ハンドラーを次に示します。

struct EventRecipient : winrt::implements<EventRecipient, IInspectable>
{
    winrt::hstring m_value{ L"Hello, World!" };

    void Register(EventSource& event_source)
    {
        event_source.Event({ this, &EventRecipient::OnEvent });
    }

    void OnEvent(IInspectable const& /* sender */, int /* args */)
    {
        std::wcout << m_value.c_str() << std::endl;
    }
};

これは、オブジェクトとそのメンバー関数を参照する標準的な従来の方法です。 これを安全にするために、Windows SDK のバージョン 10.0.17763.0 (Windows 10 バージョン 1809) の時点で、ハンドラーが登録されている時点で強い参照または弱い参照を確立できます。 その時点で、イベント受信者オブジェクトはまだ有効であることがわかっています。

強参照を取得するには、生のthisポインターの代わりにget_strongを呼び出してください。 C++/WinRT は、結果のデリゲートが現在のオブジェクトへの厳密な参照を保持することを保証します。

event_source.Event({ get_strong(), &EventRecipient::OnEvent });

強参照を保持すると、ハンドラーの登録が解除され、実行中のすべてのコールバックの処理が完了した後にのみ、そのオブジェクトは破棄可能になります。 ただし、その保証は、イベントが発生した時点でのみ有効です。 イベント ハンドラーが非同期の場合は、最初の中断ポイントの前にコルーチンにクラス インスタンスへの厳密な参照を与える必要があります (詳細とコードについては、このトピックの「 クラス メンバー コルーチンの この ポインターに安全にアクセスする 」セクションを参照してください)。 ただし、イベント ソースとオブジェクトの間に循環参照が作成されるため、イベントを取り消して明示的に中断する必要があります。

弱参照の場合は、 get_weakを呼び出します。 C++/WinRT は、結果のデリゲートが弱い参照を保持することを保証します。 最後の 1 分間とバックグラウンドで、デリゲートは弱い参照を強い参照に解決しようと試み、成功した場合にのみメンバー関数を呼び出します。

event_source.Event({ get_weak(), &EventRecipient::OnEvent });

デリゲートがメンバー関数を呼び出 場合、C++/WinRT はハンドラーが戻るまでオブジェクトを維持します。 ただし、ハンドラーが非同期の場合は中断ポイントで返されるため、最初の中断ポイントの前にコルーチンにクラス インスタンスへの厳密な参照を与える必要があります。 ここでも、詳細については、このトピック の前の「クラス メンバー コルーチン」セクションで この ポインターに安全にアクセスする 方法を参照してください。

メンバー関数がWindows ランタイム型に属していない場合

get_strong メソッドが使用できない場合 (型がWindows ランタイム型ではない場合)、次のコード例に示す手法を使用できます。 ここでは、NetworkInformation.NetworkStatusChanged イベントを処理する通常の C++ クラス (ConsoleNetworkWatcher という名前) を示します。

#include <winrt/Windows.Networking.Connectivity.h>
using namespace winrt;
using namespace Windows::Networking::Connectivity;

class ConsoleNetworkWatcher
{
    /* any constructor, and instance methods, here*/

    static void Initialize(std::shared_ptr<ConsoleNetworkWatcher> instance)
    {
        auto weakPointer{ std::weak_ptr{ instance } };

        instance->m_statusChangedRevoker =
            NetworkInformation::NetworkStatusChanged(winrt::auto_revoke,
                [weakPointer](winrt::Windows::Foundation::IInspectable const& sender)
                {
                    auto sharedPointer{ weakPointer.lock() };

                    if (sharedPointer)
                    {
                        sharedPointer->NetworkStatusChanged(sender);
                    }
                });
    }

    void NetworkStatusChanged(winrt::Windows::Foundation::IInspectable const& sender){/* handle event here */};

private:
    NetworkInformation::NetworkStatusChanged_revoker m_statusChangedRevoker;
};

SwapChainPanel::CompositionScaleChanged を使用した弱い参照例

このコード例では、弱参照の別の例として SwapChainPanel::CompositionScaleChanged イベントを利用します。 このコードでは、受信者への弱い参照をキャプチャするラムダを使用してイベント ハンドラーを登録します。

winrt::Microsoft::UI::Xaml::Controls::SwapChainPanel m_swapChainPanel;
winrt::event_token m_compositionScaleChangedEventToken;

void RegisterEventHandler()
{
    m_compositionScaleChangedEventToken = m_swapChainPanel.CompositionScaleChanged([weak_this{ get_weak() }]
        (Microsoft::UI::Xaml::Controls::SwapChainPanel const& sender,
        Windows::Foundation::IInspectable const& object)
    {
        if (auto strong_this{ weak_this.get() })
        {
            strong_this->OnCompositionScaleChanged(sender, object);
        }
    });
}

void OnCompositionScaleChanged(Microsoft::UI::Xaml::Controls::SwapChainPanel const& sender,
    Windows::Foundation::IInspectable const& object)
{
    // Here, we know that the "this" object is valid.
}

ランバ キャプチャ句では、 これに対する弱い参照を表す一時変数が作成されます。 ラムダの本体で、 これに 対する厳密な参照を取得できる場合は、 OnCompositionScaleChanged 関数が呼び出されます。 これにより、 OnCompositionScaleChangedで安全に 使用できます。

C++/WinRT の弱い参照

上記では、弱い参照が使用されています。 一般に、これらは循環参照を壊すのに適しています。 たとえば、フレームワークの歴史的な設計により、XAML ベースの UI フレームワークのネイティブ実装の場合、C++/WinRT の弱参照メカニズムは循環参照を処理するために必要です。 ただし、XAML の外部では、弱い参照を使用する必要はありません (本質的に XAML 固有のものはありません)。 むしろ、多くの場合、循環参照や弱参照の必要性を回避するように独自の C++/WinRT API を設計できるようにする必要があります。

宣言する特定の型については、弱い参照が必要かどうかに関わらず、C++/WinRT にとってすぐには明らかではありません。 そのため、C++/WinRT では、独自の C++/WinRT 型が直接または間接的に派生する、構造体テンプレート winrt::implements で弱参照サポートが自動的に提供されます。 オブジェクトが実際に IWeakReferenceSource に対してクエリを実行しない限り、料金は発生しません。 また、 そのサポートをオプトアウトすることを明示的に選択できます。

コード例

winrt::weak_ref 構造体テンプレートは、クラス インスタンスへの弱い参照を取得するための 1 つのオプションです。

Class c;
winrt::weak_ref<Class> weak{ c };

または、 winrt::make_weak ヘルパー関数を使用することもできます。

Class c;
auto weak = winrt::make_weak(c);

弱参照を作成しても、オブジェクト自体の参照カウントには影響しません。コントロール ブロックが割り当てられるだけです。 その制御ブロックは、弱参照セマンティクスの実装を処理します。 その後、弱い参照を強い参照に昇格させ、成功した場合はそれを使用することができます。

if (Class strong = weak.get())
{
    // use strong, for example strong.DoWork();
}

他の厳密な参照がまだ存在する場合、 weak_ref::get 呼び出しは参照カウントをインクリメントし、呼び出し元への厳密な参照を返します。

弱参照サポートのオプトアウト

弱参照のサポートは自動的に行われます。 ただし、 winrt::no_weak_ref マーカー構造体をテンプレート引数として基底クラスに渡すことで、そのサポートをオプトアウトすることを明示的に選択できます。

winrt::implements を直接継承する場合。

struct MyImplementation: implements<MyImplementation, IStringable, no_weak_ref>
{
    ...
}

ランタイム クラスを作成している場合。

struct MyRuntimeClass: MyRuntimeClassT<MyRuntimeClass, no_weak_ref>
{
    ...
}

可変数パラメーター パック内のどこにマーカー構造体が表示されるかは関係ありません。 オプトアウトされている型に対して弱参照を要求すると、コンパイラは「これは弱参照のサポート専用です」というメッセージを表示して知らせてくれます。

重要な API