Analisis 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 membahas secara mendalam tentang fitur C++/WinRT 2.0 yang membantu Anda mendiagnosis kesalahan karena mengalokasikan objek bertipe implementasi secara langsung di stack.

Kesalahan semacam itu dapat menyebabkan kegagalan sistem atau kerusakan data misterius yang sulit dan memakan waktu untuk ditelusuri penyebabnya. Jadi ini adalah fitur penting, dan ada baiknya memahami latar belakang.

Mengatur adegan, dengan MyStringable

Pertama, mari kita pertimbangkan implementasi sederhana dari 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 tipe MyStringable kami bukan sebuah IStringable.

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

Important

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.

Perbedaan antara implementasi dan proyeksi bisa jadi sulit dipahami. Dan sebenarnya, agar implementasi terasa sedikit lebih mirip dengan proyeksi, implementasi tersebut menyediakan konversi implisit ke masing-masing tipe proyeksi yang diimplementasikannya. 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 templat fungsi winrt::make (atau winrt::make_self) untuk mengalokasikan implementasi, maka semuanya baik-baik saja.

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

Potensi masalah pada 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. Masalahnya (sangat serius) adalah kita berpotensi mengembalikan tipe proyeksi yang menunjuk ke objek dengan penghitungan referensi yang memori pendukungnya berada di stack sementara.

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

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

Raw pointer berbahaya dan menjadi sumber bug yang memerlukan banyak upaya. Jangan gunakan jika Anda tidak perlu. C++/WinRT berupaya keras 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 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 hidup bersama dari jumlah referensi intrinsik milik MyStringable.

Solusi dengan C++/WinRT 2.0

Dengan C++/WinRT 2.0, semua upaya ini untuk langsung mengalokasikan jenis implementasi menyebabkan kesalahan kompilator. Itulah jenis kesalahan terbaik, dan jauh lebih baik daripada bug runtime yang 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 sebuah static_assert; tetapi hampir sama. 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 ketiadaan override untuk mendeteksi alokasi secara langsung, templat fungsi winrt::make harus entah bagaimana menyediakan implementasi untuk fungsi virtual abstrak tersebut melalui sebuah override. Hal ini dilakukan dengan menurunkan dari implementasi menggunakan kelas final yang menyediakan penimpaan. Ada beberapa hal yang perlu diamati tentang proses ini.

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

Kedua, karena kelas turunan yang digunakan oleh winrt::make adalah final, itu berarti bahwa setiap devirtualisasi yang dapat disimpulkan oleh pengoptimal akan tetap terjadi, bahkan jika sebelumnya Anda memilih untuk tidak menandai kelas implementasi Anda sebagai final. Jadi itu adalah perbaikan. Sebaliknya, implementasi Anda tidak bisafinal. Sekali lagi, itu tidak berpengaruh karena tipe yang diinstansiasi akan selalu berupa final.

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 hampir tidak ada alasan untuk menggunakan fungsi virtual dalam implementasi C++/WinRT Anda. Ini merupakan hal yang sangat baik dan membuat inlining jauh lebih mudah diprediksi.

Keempat, karena winrt::make menyuntikkan kelas turunan, implementasi Anda tidak dapat memiliki destruktor privat. Destruktor privat populer dalam implementasi COM klasik karena, sekali lagi, semuanya bersifat virtual, dan orang lazim berurusan langsung dengan pointer mentah sehingga mudah secara tidak sengaja memanggil delete alih-alih Release. C++/WinRT sengaja berupaya keras untuk mempersulit Anda berurusan langsung dengan pointer mentah. Dan Anda harus benar-benar bersusah payah untuk mendapatkan pointer mentah di C++/WinRT yang mungkin 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 rakitan bagi 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