Nuta
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować się zalogować lub zmienić katalog.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
W tym temacie omówiono punkty rozszerzenia winrt::implements w języku C++/WinRT 2.0. Możesz zaimplementować te punkty rozszerzenia dla typów implementacji, aby dostosować domyślne zachowanie obiektów, które można sprawdzić (sprawdzalne w sensie interfejsu IInspectable).
Te punkty rozszerzenia umożliwiają odroczenie zniszczenia typów implementacji, bezpieczne wykonywanie zapytań podczas niszczenia oraz podłączanie wejścia do i wyjścia z przewidywanych metod. W tym temacie opisano te funkcje i wyjaśniono więcej na temat tego, kiedy i jak można ich używać.
Odroczone zniszczenie
W temacie Diagnostyce alokacji bezpośrednich wspomnieliśmy, że typ implementacji nie może mieć prywatnego destruktora.
Zaletą posiadania publicznego destruktora jest to możliwość wykrycia ostatecznego wywołania IUnknown::Release na twoim obiekcie, a następnie przejęcie tego obiektu w celu odroczenia jego zniszczenia na czas nieokreślony.
Pamiętaj, że klasyczne obiekty COM mają wewnętrznie licznik odwołań; liczba ta jest zarządzana za pomocą funkcji IUnknown::AddRef i IUnknown::Release. W tradycyjnej implementacji Release destruktor C++ klasycznego obiektu COM jest wywoływany, gdy liczba odwołań spada do zera.
uint32_t WINRT_CALL Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
delete this;
}
return remaining;
}
delete this; wywołuje destruktor obiektu przed zwolnieniem pamięci używanej przez obiekt. To działa wystarczająco dobrze, pod warunkiem, że nie musisz wykonywać żadnych interesujących czynności w destruktorze.
using namespace winrt::Windows::Foundation;
...
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
~Sample() noexcept
{
// Too late to do anything interesting.
}
};
Co mamy na myśli przez interesujące? Dla jednej rzeczy destruktor jest z natury synchroniczny. Nie można przełączać wątków — być może zniszczyć niektóre zasoby specyficzne dla wątku w innym kontekście. Nie można w sposób niezawodny zapytać obiektu o inny interfejs, który może być potrzebny do zwolnienia niektórych zasobów. Lista jest dłuższa. W przypadkach, w których zniszczenie nie jest trywialne, potrzebujesz bardziej elastycznego rozwiązania. Właśnie tu wkracza funkcja final_release języka C++/WinRT.
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.
}
};
Zaktualizowaliśmy implementację języka C++/WinRT Release w celu wywołania final_release dokładnie wtedy, gdy liczba odniesień do obiektu zmienia się na 0. W tym stanie obiekt może być pewny, że nie ma dalszych niezamkniętych referencji, i teraz posiada pełną kontrolę nad sobą. Z tego powodu może przenieść swoją własność do funkcji statycznej final_release.
Innymi słowy, obiekt przekształcił się z obiektu wspierającego współwłasność w taki, który jest własnością wyłącznie. std::unique_ptr ma wyjątkową własność obiektu, więc zgodnie z semantyką obiekt zostanie zniszczony — stąd potrzeba publicznego destruktora — gdy std::unique_ptr wyjdzie poza zakres (o ile nie zostanie wcześniej przeniesiony gdzie indziej). I to jest klucz. Można użyć obiektu na czas nieokreślony, pod warunkiem, że obiekt std::unique_ptr utrzymuje się przy życiu. Oto ilustracja przedstawiająca sposób przenoszenia obiektu w innym miejscu.
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));
}
};
Ten kod zapisuje obiekt w kolekcji o nazwie batch_cleanup, której jednym z zadań będzie czyszczenie wszystkich obiektów w przyszłym momencie podczas działania aplikacji.
Zwykle obiekt jest niszczony, gdy std::unique_ptr jest likwidowany, ale można przyspieszyć jego usunięcie, wywołując std::unique_ptr::reset, lub można to opóźnić, zapisując std::unique_ptr gdzieś.
Być może bardziej praktycznie i bardziej potężnie, można przekształcić funkcję final_release w koprutynę i obsłużyć jego ostateczne zniszczenie w jednym miejscu, jednocześnie będąc w stanie zawiesić i przełączać wątki zgodnie z potrzebami.
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.
}
};
Zawieszenie spowoduje, że wątek wywołujący — który pierwotnie zainicjował wywołanie funkcji IUnknown::Release — powraca, a tym samym sygnalizuje wywołującemu, że obiekt, który kiedyś posiadał, nie jest już dostępny za pośrednictwem tego wskaźnika interfejsu. Struktury interfejsu użytkownika często wymagają zapewnienia, że obiekty są niszczone w określonym wątku interfejsu użytkownika, który pierwotnie utworzył obiekt. Ta funkcja sprawia, że spełnianie takiego wymagania jest proste, ponieważ zniszczenie jest oddzielone od uwolnienia obiektu.
Należy zauważyć, że obiekt przekazany do final_release jest tylko obiektem C++; nie jest to już obiekt COM. Na przykład istniejące słabe odwołania COM do obiektu nie są już rozpatrywane.
Bezpieczne zapytania podczas niszczenia
Opierając się na pojęciu odroczonego zniszczenia, jest możliwość bezpiecznego wykonywania zapytań o interfejsy podczas niszczenia.
Klasyczny model COM opiera się na dwóch centralnych pojęciach. Pierwszy to zliczanie odwołań, a drugi to wykonywanie zapytań dotyczących interfejsów. Oprócz AddRef i Release, interfejs IUnknown zapewnia QueryInterface. Ta metoda jest intensywnie używana przez niektóre struktury interfejsu użytkownika — takie jak XAML, aby przejść przez hierarchię XAML, gdy symuluje system typów komponowalnych. Rozważmy prosty przykład.
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
};
Może to wydawać się nieszkodliwe. Ta strona XAML chce wyczyścić kontekst danych w destruktorze. Jednak element DataContext jest właściwością klasy bazowej FrameworkElement i znajduje się w osobnym interfejsie IFrameworkElement . W związku z tym, C++/WinRT musi umieścić wywołanie QueryInterface, aby znaleźć poprawną tabelę wirtualną, zanim będzie można wywołać właściwość DataContext. Ale powód, dla którego w ogóle jesteśmy w destruktorze, to fakt, że liczba odwołań zmniejszyła się do 0. Wywołanie QueryInterface tymczasowo zwiększa licznik referencji, a gdy ponownie spadnie on do zera, obiekt zostanie ponownie zdestruowany.
Język C++/WinRT 2.0 został wzmocniony, aby to obsługiwać. Oto uproszczona wersja implementacji funkcji Release w 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;
}
Jak można było przewidzieć, najpierw dekrementuje liczbę odwołań, a następnie działa tylko wtedy, gdy nie ma zaległych odwołań. Jednak przed wywołaniem funkcji statycznej final_release opisanej wcześniej w tym temacie funkcja stabilizuje liczbę odwołań, ustawiając ją na 1. Nazywamy to debouncing (pożyczanie terminu od inżynierii elektrycznej). Jest to kluczowe, aby zapobiec zwolnieniu ostatecznej referencji. Gdy tak się stanie, liczba odwołań jest niestabilna i nie jest w stanie niezawodnie obsługiwać wywołania QueryInterface.
Wywoływanie QueryInterface jest niebezpieczne po wydaniu ostatecznego odwołania, ponieważ liczba odwołań może następnie rosnąć w nieskończoność. Twoim zadaniem jest wywołanie tylko znanych ścieżek kodu, które nie przedłużają życia obiektu. Język C++/WinRT spełnia Twoje oczekiwania w połowie, upewniając się, że te QueryInterface wywołania mogą być wykonane niezawodnie.
Czyni to, stabilizując liczbę odwołań. Po wydaniu ostatniego odniesienia rzeczywista liczba odniesień wynosi 0 lub jakaś szalenie nieprzewidywalna wartość. Ostatni przypadek może wystąpić, jeśli jest związany ze słabymi referencjami. Tak czy inaczej, jest to nie do utrzymania w dłuższej perspektywie, jeśli wystąpi kolejne wywołanie QueryInterface; ponieważ spowoduje to tymczasowe zwiększenie liczby odwołań — stąd odwołanie do zjawiska debouncingu. Ustawienie wartości 1 zapewnia, że ostatnie wywołanie Release już nie wystąpi na tym obiekcie. To właśnie chcemy, ponieważ std::unique_ptr jest teraz właścicielem obiektu, ale powiązane wywołania do QueryInterface/Release par będą bezpieczne.
Rozważ bardziej interesujący przykład.
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;
}
};
Najpierw wywoływana jest funkcja final_release z powiadomieniem implementacji, że nadszedł czas na wyczyszczenie. W tym miejscu final_release jest kohroutyną. Aby zasymulować pierwszy punkt zawieszenia, zaczyna się od oczekiwania na zasoby puli wątków przez kilka sekund. Następnie zostanie wznowiona w wątku dyspozytora strony. Ten ostatni krok obejmuje zapytanie, ponieważ Dispatcher jest właściwością klasy bazowej DependencyObject . Na koniec strona jest faktycznie usuwana przez przypisanie nullptr do std::unique_ptr. To z kolei wywołuje destruktor strony.
Wewnątrz destruktora wyczyścimy kontekst danych; które, jak wiemy, wymaga zapytania dla FrameworkElement klasy bazowej.
Wszystko to możliwe dzięki stabilizacji licznika referencji (lub jego stabilizacji) zapewnionej przez C++/WinRT 2.0.
Punkty zaczepienia dla wejścia i wyjścia metody
Rzadziej używanym punktem rozszerzenia jest struktura abi_guard oraz funkcje abi_enter i abi_exit .
Jeśli typ implementacji definiuje funkcję abi_enter, ta funkcja jest wywoływana przy wejściu do każdej z projektowanych metod interfejsu (nie licząc metod IInspectable).
Podobnie, jeśli zdefiniujesz abi_exit, zostanie ona wywołana przy wyjściu z każdej takiej metody; ale nie zostanie wywołana, jeśli abi_enter zgłosi wyjątek. będzie ona nadal wywoływana, jeśli zostanie zgłoszony wyjątek przez projektowaną metodę interfejsu.
Na przykład można użyć abi_enter, aby zgłosić hipotetyczny wyjątek invalid_state_error, jeśli klient spróbuje użyć obiektu po przejściu obiektu w stan niefunkcjonalny — na przykład po wywołaniu metody ShutDown lub metody Rozłącz. Klasy iteracyjne C++/WinRT używają tej funkcji do zgłaszania nieprawidłowego wyjątku stanu w funkcji abi_enter , jeśli podstawowa kolekcja uległa zmianie.
Oprócz prostych funkcji abi_enter i abi_exitmożna zdefiniować typ zagnieżdżony o nazwie abi_guard. W takim przypadku wystąpienie abi_guard jest tworzone przy wejściu do każdej (nie-IInspectable) z metod projekcji interfejsu, z odwołaniem do obiektu jako parametr jego konstruktora. abi_guard jest następnie niszczony po wyjściu z metody. Możesz umieścić dowolny dodatkowy stan w typie abi_guard.
Jeśli nie zdefiniujesz własnego abi_guard, istnieje domyślny, który wywołuje abi_enter przy tworzeniu i abi_exit przy zniszczeniu.
Te zabezpieczenia są używane tylko wtedy, gdy metoda jest wywoływana za pośrednictwem przewidywanego interfejsu. Jeśli metody są wywoływane bezpośrednio w obiekcie implementacji, wywołania te przechodzą bezpośrednio do implementacji bez żadnych zabezpieczeń.
Oto przykład kodu.
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.
}