Poin ekstensi untuk jenis implementasi Anda

Template struct winrt::implements adalah dasar dari mana implementasi C ++/WinRT Anda sendiri (kelas runtime dan pabrik aktivasi) secara langsung atau tidak langsung berasal.

Topik ini membahas poin ekstensi winrt::implements di C++/WinRT 2.0. Anda dapat memilih untuk menerapkan poin ekstensi ini pada jenis implementasi Anda, untuk menyesuaikan perilaku default objek yang dapat diperiksa (dapat diperiksa dalam arti antarmuka IInspectable ).

Poin ekstensi ini memungkinkan Anda untuk menunda penghancuran jenis implementasi Anda, untuk meminta dengan aman selama penghancuran, dan untuk menghubungkan masuk dan keluar dari metode yang diproyeksikan. Topik ini menjelaskan fitur-fitur tersebut dan menjelaskan lebih lanjut tentang kapan dan bagaimana Anda akan menggunakannya.

Penghancuran yang ditangguhkan

Dalam topik Mendiagnosis alokasi langsung , kami menyebutkan bahwa jenis implementasi Anda tidak dapat memiliki perusakan pribadi.

Manfaat dari memiliki destructor publik adalah bahwa hal itu memungkinkan kehancuran ditangguhkan, yang merupakan kemampuan untuk mendeteksi IUnknown akhir: : Lepaskan panggilan pada objek Anda, dan kemudian mengambil kepemilikan objek itu untuk menunda kehancurannya tanpa batas waktu.

Ingatlah bahwa objek COM klasik secara intrinsik dihitung referensi; jumlah referensi dikelola melalui fungsi IUnknown::AddRef dan IUnknown::Release . Dalam implementasi tradisional Release, perusakan C ++ objek COM klasik dipanggil setelah jumlah referensi mencapai 0.

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

Memanggil delete this; destructor objek sebelum membebaskan memori yang ditempati oleh objek. Ini bekerja cukup baik, asalkan Anda tidak perlu melakukan sesuatu yang menarik dalam destructor Anda.

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

Apa yang dimaksud dengan menarik? Untuk satu hal, destructor secara inheren sinkron. Anda tidak dapat beralih utas—mungkin untuk menghancurkan beberapa sumber daya khusus utas dalam konteks yang berbeda. Anda tidak dapat mengkueri objek dengan andal untuk beberapa antarmuka lain yang mungkin Anda perlukan untuk membebaskan sumber daya tertentu. Daftarnya terus berlanjut. Untuk kasus-kasus di mana kehancuran Anda tidak sepele, Anda memerlukan solusi yang lebih fleksibel. Di situlah fungsi final_release C ++/WinRT masuk.

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.
    }
};

Kami telah memperbarui implementasi Rilis C++/WinRT untuk memanggil final_release Anda tepat ketika jumlah referensi objek Anda bertransisi ke 0. Dalam keadaan itu, objek dapat yakin bahwa tidak ada referensi luar biasa lebih lanjut, dan sekarang memiliki kepemilikan eksklusif atas dirinya sendiri. Untuk alasan itu, ia dapat mentransfer kepemilikan dirinya ke fungsi final_release statis.

Dengan kata lain, objek telah mengubah dirinya dari objek yang mendukung kepemilikan bersama menjadi milik eksklusif. Std::unique_ptr memiliki kepemilikan eksklusif atas objek, sehingga secara alami akan menghancurkan objek sebagai bagian dari semantiknya — oleh karena itu perlunya perusakan publik — ketika std: unique_ptr keluar dari ruang lingkup (asalkan tidak dipindahkan ke tempat lain sebelum itu). Dan itulah kuncinya. Anda dapat menggunakan objek tanpa batas waktu, asalkan std::unique_ptr menjaga objek tetap hidup. Berikut adalah ilustrasi tentang bagaimana Anda bisa memindahkan objek ke tempat lain.

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

Anggap ini sebagai pengumpul sampah yang lebih deterministik.

Biasanya, objek hancur ketika std::unique_ptr merusak, tetapi Anda dapat mempercepat kehancurannya dengan memanggil std::unique_ptr::reset; atau Anda dapat menundanya dengan menyimpan std::unique_ptr di suatu tempat.

Mungkin lebih praktis dan lebih kuat, Anda dapat mengubah fungsi final_release menjadi coroutine, dan menangani kehancuran akhirnya di satu tempat sambil dapat menangguhkan dan mengganti benang sesuai kebutuhan.

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.
    }
};

Penangguhan akan menunjuk menyebabkan utas panggilan — yang awalnya memulai panggilan ke fungsi IUnknown::Release —untuk kembali, dan dengan demikian memberi sinyal kepada penelepon bahwa objek yang pernah dipegangnya tidak lagi tersedia melalui penunjuk antarmuka itu. Kerangka kerja UI sering perlu memastikan bahwa objek dihancurkan pada utas UI tertentu yang awalnya membuat objek. Fitur ini membuat pemenuhan persyaratan seperti itu sepele, karena kehancuran dipisahkan dari melepaskan objek.

Brankas pertanyaan selama penghancuran

Membangun gagasan penghancuran yang ditangguhkan adalah kemampuan untuk meminta antarmuka dengan aman selama penghancuran.

Classic COM didasarkan pada dua konsep sentral. Yang pertama adalah penghitungan referensi, dan yang kedua adalah query untuk antarmuka. Selain AddRef dan Release, antarmuka IUnknown menyediakan QueryInterface. Metode itu banyak digunakan oleh kerangka kerja UI tertentu — seperti XAML, untuk melintasi hierarki XAML saat mensimulasikan sistem tipe composable-nya. Pertimbangkan contoh sederhana.

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

Itu mungkin tampak tidak berbahaya. Halaman XAML ini ingin menghapus konteks datanya dalam destructornya. Tetapi DataContext adalah properti dari kelas dasar FrameworkElement , dan hidup di antarmuka IFrameworkElement yang berbeda. Akibatnya, C ++/WinRT harus menyuntikkan panggilan ke QueryInterface untuk mencari vtable yang benar sebelum dapat memanggil properti DataContext . Tetapi alasan kita bahkan berada dalam destructor adalah bahwa jumlah referensi telah beralih ke 0. Memanggil QueryInterface di sini untuk sementara menabrak jumlah referensi itu; dan ketika kembali ke 0, objek hancur lagi.

C ++/WinRT 2.0 telah dikeraskan untuk mendukung hal ini. Berikut adalah implementasi Rilis C ++/WinRT 2.0, dalam bentuk yang disederhanakan.

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

Seperti yang mungkin telah Anda prediksi, pertama-tama mengurangi jumlah referensi, dan kemudian bertindak hanya jika tidak ada referensi yang beredar. Namun, sebelum memanggil fungsi final_release statis yang kami jelaskan sebelumnya dalam topik ini, itu menstabilkan jumlah referensi dengan mengaturnya menjadi 1. Kami menyebut ini sebagai debouncing (meminjam istilah dari teknik elektro). Ini sangat penting untuk mencegah referensi akhir dilepaskan. Setelah itu terjadi, jumlah referensi tidak stabil, dan tidak dapat mendukung panggilan ke QueryInterface dengan andal.

Memanggil QueryInterface berbahaya setelah referensi akhir dirilis, karena jumlah referensi kemudian dapat tumbuh tanpa batas waktu. Adalah tanggung jawab Anda untuk hanya memanggil jalur kode yang diketahui yang tidak akan memperpanjang umur objek. C ++/WinRT memenuhi Anda di tengah jalan dengan memastikan bahwa panggilan QueryInterface tersebut dapat dilakukan dengan andal.

Itu dilakukan dengan menstabilkan jumlah referensi. Ketika referensi akhir telah dirilis, jumlah referensi aktual adalah 0, atau nilai yang sangat tidak dapat diprediksi. Kasus terakhir dapat terjadi jika referensi yang lemah terlibat. Either way, ini tidak berkelanjutan jika panggilan berikutnya ke QueryInterface terjadi; karena itu tentu akan menyebabkan jumlah referensi meningkat sementara — maka referensi untuk debouncing. Mengaturnya ke 1 memastikan bahwa panggilan terakhir ke Rilis tidak akan pernah lagi terjadi pada objek ini. Itulah yang kami inginkan, karena std::unique_ptr sekarang memiliki objek, tetapi panggilan terbatas ke pasangan QueryInterfaceRelease/ akan aman.

Pertimbangkan contoh yang lebih menarik.

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;
    }
};

Pertama, fungsi final_release dipanggil, memberi tahu implementasi bahwa sudah waktunya untuk membersihkan. Di sini, final_release kebetulan adalah coroutine. Untuk mensimulasikan titik suspensi pertama, itu dimulai dengan menunggu di kolam benang selama beberapa detik. Kemudian dilanjutkan pada utas operator halaman. Langkah terakhir itu melibatkan kueri, karena Dispatcher adalah properti dari kelas dasar DependencyObject . Akhirnya, halaman tersebut benar-benar dihapus berdasarkan penetapan nullptr ke std::unique_ptr. Itu pada gilirannya memanggil destructor halaman.

Di dalam destructor, kami menghapus konteks data; yang, seperti yang kita tahu, memerlukan kueri untuk kelas dasar FrameworkElement .

Semua ini mungkin karena debouncing hitungan referensi (atau stabilisasi jumlah referensi) yang disediakan oleh C ++/WinRT 2.0.

Kait masuk dan keluar metode

Titik ekstensi yang kurang umum digunakan adalah abi_guard struct, dan fungsi abi_enter dan abi_exit .

Jika jenis implementasi Anda mendefinisikan fungsi abi_enter, maka fungsi itu dipanggil pada entri ke semua metode antarmuka yang diproyeksikan Anda (tidak termasuk metode IInspectable).

Demikian pula, jika Anda mendefinisikan abi_exit, maka itu akan dipanggil di pintu keluar dari setiap metode tersebut; tetapi itu tidak akan dipanggil jika abi_enter Anda memberikan pengecualian. Ini masih akan dipanggil jika pengecualian dilemparkan oleh metode antarmuka yang diproyeksikan itu sendiri.

Misalnya, Anda mungkin menggunakan abi_enter untuk memberikan pengecualian invalid_state_error hipotetis jika klien mencoba menggunakan objek setelah objek dimasukkan ke dalam keadaan yang tidak dapat digunakan—katakanlah setelah panggilan metode ShutDown atau Disconnect . Kelas iterator C ++/WinRT menggunakan fitur ini untuk memberikan pengecualian status yang tidak valid dalam fungsi abi_enter jika koleksi yang mendasarinya telah berubah.

Di atas dan di atas abi_enter sederhana dan abi_exit fungsi, Anda dapat menentukan jenis bersarang bernama abi_guard. Dalam hal ini, contoh abi_guard dibuat saat masuk ke masing-masing (non-IInspectable) dari metode antarmuka yang diproyeksikan, dengan referensi ke objek sebagai parameter konstruktornya. abi_guard kemudian dihancurkan saat keluar dari metode. Anda dapat menempatkan keadaan ekstra apa pun yang Anda sukai ke dalam jenis abi_guard Anda.

Jika Anda tidak mendefinisikan abi_guard Anda sendiri, maka ada default yang memanggil abi_enter saat konstruksi, dan abi_exit kehancuran.

Penjaga ini hanya digunakan ketika metode dipanggil melalui antarmuka yang diproyeksikan. Jika Anda memanggil metode langsung pada objek implementasi, maka panggilan tersebut langsung ke implementasi, tanpa penjaga.

Berikut adalah contoh kode.

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.
}