Bagikan melalui


Mendiagnosis alokasi langsung

Seperti yang dijelaskan dalam API Penulis dengan C++/WinRT, ketika Anda membuat objek jenis implementasi, Anda harus menggunakan keluarga pembantu winrt::make untuk melakukannya. Topik ini secara mendalam pada fitur C++/WinRT 2.0 yang membantu Anda mendiagnosis kesalahan mengalokasikan objek jenis implementasi secara langsung pada tumpukan.

Kesalahan tersebut dapat berubah menjadi crash atau kerusakan misterius yang sulit dan memakan waktu untuk debug. Jadi ini adalah fitur penting, dan ada baiknya memahami latar belakang.

Mengatur adegan, dengan MyStringable

Pertama, mari kita pertimbangkan implementasi sederhana IStringable.

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

Sekarang bayangkan bahwa Anda perlu memanggil fungsi (dari dalam implementasi Anda) yang mengharapkan IStringable sebagai argumen.

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

Masalahnya adalah bahwa jenis MyStringable kami bukan IStringable.

  • Jenis MyStringable kami adalah implementasi antarmuka IStringable.
  • Jenis IStringable adalah jenis yang diproyeksikan.

Penting

Penting untuk memahami perbedaan antara jenis implementasi dan jenis yang diproyeksikan. Untuk konsep dan istilah penting, pastikan untuk membaca Gunakan API dengan C++/WinRT dan API Penulis dengan C++/WinRT.

Ruang antara implementasi dan proyeksi dapat menjadi halang untuk diganggu. Dan pada kenyataannya, untuk mencoba membuat implementasi terasa sedikit lebih seperti proyeksi, implementasi memberikan konversi implisit ke masing-masing jenis yang diproyeksikan yang diterapkannya. Itu tidak berarti kita bisa melakukan ini.

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

Sebagai gantinya, kita perlu mendapatkan referensi sehingga operator konversi dapat digunakan sebagai kandidat untuk menyelesaikan panggilan.

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

Itu berhasil. Konversi implisit memberikan konversi (sangat efisien) dari jenis implementasi ke jenis yang diproyeksikan, dan itu sangat nyaman untuk banyak skenario. Tanpa fasilitas itu, banyak jenis implementasi akan terbukti sangat rumit bagi penulis. Asalkan Anda hanya menggunakan template fungsi winrt::make (atau winrt::make_self) untuk mengalokasikan implementasi, maka semuanya baik-baik saja.

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

Potensi perangkap dengan C++/WinRT 1.0

Namun, konversi implisit dapat membuat Anda dalam kesulitan. Pertimbangkan fungsi pembantu yang tidak membantu ini.

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

Atau bahkan pernyataan ini rupanya tidak berbahaya.

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

Sayangnya, kode seperti itu memang dikompilasi dengan C++/WinRT 1.0, karena konversi implisit itu. Masalah (sangat serius) adalah bahwa kita berpotensi mengembalikan jenis yang diproyeksikan yang menunjuk ke objek yang dihitung referensi yang memori cadangannya ada di tumpukan sementara.

Berikut adalah sesuatu yang lain yang dikompilasi dengan C++/WinRT 1.0.

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

Pointer mentah berbahaya dan sumber bug padat karya. Jangan gunakan jika Anda tidak perlu. C++/WinRT keluar dari caranya untuk membuat semuanya efisien tanpa pernah memaksa Anda menggunakan pointer mentah. Berikut adalah sesuatu yang lain yang dikompilasi dengan C++/WinRT 1.0.

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

Ini adalah kesalahan pada beberapa level. Kami memiliki dua jumlah referensi yang berbeda untuk objek yang sama. Windows Runtime (dan COM klasik sebelumnya) didasarkan pada jumlah referensi intrinsik yang tidak kompatibel dengan std::shared_ptr. std::shared_ptr memiliki, tentu saja, banyak aplikasi yang valid; tetapi sama sekali tidak perlu ketika Anda berbagi objek Windows Runtime (dan COM klasik). Terakhir, ini juga dikompilasi dengan C++/WinRT 1.0.

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

Ini lagi-lagi agak dipertanyakan. Kepemilikan unik bertentangan dengan masa pakai bersama jumlah referensi intrinsik MyStringable.

Solusi dengan C++/WinRT 2.0

Dengan C++/WinRT 2.0, semua upaya ini untuk langsung mengalokasikan jenis implementasi menyebabkan kesalahan kompilator. Itu adalah jenis kesalahan terbaik, dan jauh lebih baik daripada bug runtime misterius.

Setiap kali Anda perlu membuat implementasi, Anda cukup menggunakan winrt::make atau winrt::make_self, seperti yang ditunjukkan di atas. Dan sekarang, jika Anda lupa melakukannya, maka Anda akan disambut dengan kesalahan kompilator yang menyinggung hal ini dengan referensi ke fungsi abstrak bernama use_make_function_to_create_this_object. Ini tidak persis ; static_asserttetapi dekat. Namun, ini adalah cara yang paling dapat diandalkan untuk mendeteksi semua kesalahan yang dijelaskan.

Ini berarti bahwa kita perlu menempatkan beberapa batasan kecil pada implementasi. Mengingat bahwa kita mengandalkan tidak adanya penimpaan untuk mendeteksi alokasi langsung, templat fungsi winrt::make harus entah bagaimana memenuhi fungsi virtual abstrak dengan penimpaan. Hal ini dilakukan dengan berasal dari implementasi dengan final kelas yang memberikan penimpaan. Ada beberapa hal yang perlu diamati tentang proses ini.

Pertama, fungsi virtual hanya ada dalam build debug. Yang berarti bahwa deteksi tidak akan memengaruhi ukuran vtable dalam build yang dioptimalkan.

Kedua, karena kelas turunan yang winrt::make uses adalah final, itu berarti bahwa devirtualisasi apa pun yang mungkin dapat disimpulkan pengoptimal akan terjadi bahkan jika Anda sebelumnya memilih untuk tidak menandai kelas implementasi Anda sebagai final. Jadi itu adalah perbaikan. Sebaliknya adalah bahwa implementasi Anda tidak boleh final. Sekali lagi, itu bukan konsekuensi karena jenis yang dibuat akan finalselalu .

Ketiga, tidak ada yang mencegah Anda menandai fungsi virtual apa pun dalam implementasi Anda sebagai final. Tentu saja, C++/WinRT sangat berbeda dari COM klasik dan implementasi seperti WRL, di mana segala sesuatu tentang implementasi Anda cenderung virtual. Di C++/WinRT, pengiriman virtual terbatas pada antarmuka biner aplikasi (ABI) (yang selalu final), dan metode implementasi Anda mengandalkan polimorfisme kompilasi atau statis. Itu menghindari polimorfisme runtime yang tidak perlu, dan juga berarti bahwa ada sedikit alasan berharga untuk fungsi virtual dalam implementasi C++/WinRT Anda. Yang merupakan hal yang sangat baik, dan mengarah ke inlining yang jauh lebih dapat diprediksi.

Keempat, karena winrt::make menyuntikkan kelas turunan, implementasi Anda tidak dapat memiliki destruktor privat. Destruktor privat populer dengan implementasi COM klasik karena, sekali lagi, semuanya virtual, dan umum untuk berurusan langsung dengan pointer mentah dan dengan demikian mudah untuk secara tidak sengaja memanggil delete alih-alih Rilis. C++/WinRT keluar dari jalannya untuk membuat Anda sulit untuk berurusan langsung dengan pointer mentah. Dan Anda harus benar-benar keluar dari cara Anda untuk mendapatkan pointer mentah di C++/WinRT yang berpotensi Anda panggil delete . Semantik nilai berarti Anda berhadapan dengan nilai dan referensi; dan jarang dengan pointer.

Jadi, C++/WinRT menantang gagasan kami yang telah ditentukan sebelumnya tentang apa artinya menulis kode COM klasik. Dan itu sangat masuk akal karena WinRT bukan COM klasik. COM klasik adalah bahasa perakitan Windows Runtime. Seharusnya bukan kode yang Anda tulis setiap hari. Sebaliknya, C++/WinRT membuat Anda menulis kode yang lebih seperti C++modern, dan jauh lebih sedikit seperti COM klasik.

API penting