Udostępnij przez


Diagnozowanie alokacji bezpośrednich

Jak wyjaśniono w Author APIs with C++/WinRT, podczas tworzenia obiektu typu implementacji należy użyć winrt::make rodziny pomocników, aby to zrobić. Ten temat szczegółowo omawia funkcję języka C++/WinRT 2.0, która ułatwia diagnozowanie błędu bezpośredniego przydzielania obiektu typu implementacji na stosie.

Takie błędy mogą przekształcić się w tajemnicze awarie lub uszkodzenia, które są trudne i czasochłonne do debugowania. Jest to więc ważna funkcja i warto zrozumieć tło.

Ustawianie sceny przy użyciu MyStringable

Najpierw rozważmy prostą implementację IStringable.

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

Teraz wyobraź sobie, że musisz wywołać funkcję (z poziomu implementacji), która oczekuje IStringable jako argumentu.

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

Problem polega na tym, że nasz typ myStringable nie jest IStringable.

  • Nasz typ MyStringable to implementacja interfejsu IStringable .
  • Typ IStringable jest typem przewidywanym.

Ważne

Ważne jest, aby zrozumieć rozróżnienie między typem implementacji a przewidywanym typem. Aby zapoznać się z podstawowymi pojęciami i terminami, koniecznie przeczytaj Konsumowanie interfejsów API z C++/WinRT oraz Tworzenie interfejsów API z C++/WinRT.

Przestrzeń między implementacją a projekcją może być subtelna do uchwycenia. W rzeczywistości, aby implementacja bardziej przypominała projekcję, implementacja zapewnia niejawne konwersje do każdego z typów projekcyjnych, które są w niej implementowane. To nie znaczy, że możemy to po prostu zrobić.

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

Zamiast tego musimy uzyskać referencję, aby operatory konwersji mogły być używane jako kandydaci do rozwiązywania wywołania.

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

To działa. Konwersja niejawna zapewnia (bardzo wydajną) konwersję z typu implementacji na typ przewidywany i jest to bardzo wygodne w przypadku wielu scenariuszy. Bez tego obiektu wiele typów implementacji okaże się bardzo kłopotliwe dla autora. Jeśli używasz tylko szablonu funkcji winrt::make (lub winrt::make_self), aby przydzielić implementację, wszystko jest dobre.

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

Potencjalne pułapki w języku C++/WinRT 1.0

Mimo to niejawne konwersje mogą sprawić ci kłopoty. Rozważ tę nieprzydatną funkcję pomocnika.

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

A nawet tylko to najwyraźniej nieszkodliwe stwierdzenie.

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

Niestety kod podobny do tego skompilowany przy użyciu języka C++/WinRT 1.0 z powodu tej niejawnej konwersji. (bardzo poważny) problem polega na tym, że potencjalnie zwracamy projektowany typ, który wskazuje na obiekt zliczany referencyjnie, którego pamięć zaplecza znajduje się na stosie efemerycznym.

Oto coś innego kompilowanego z językiem C++/WinRT 1.0.

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

Surowe wskaźniki w języku C++ są niebezpiecznym i kosztownym pod względem pracy źródłem błędów. Nie używaj ich, jeśli nie musisz tego robić. C++/WinRT dokłada wszelkich starań, aby wszystko było wydajne, nie zmuszając do korzystania z surowych wskaźników. Oto coś innego kompilowanego z językiem C++/WinRT 1.0.

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

Jest to błąd na kilku poziomach. Mamy dwie różne liczby odwołań dla tego samego obiektu. Środowisko uruchomieniowe systemu Windows (i klasyczny com przed nim) jest oparte na wewnętrznej liczbie odwołań, która nie jest zgodna z std::shared_ptr. std::shared_ptr ma oczywiście wiele uzasadnionych zastosowań, ale jest całkowicie zbędny, gdy udostępniasz obiekty środowiska uruchomieniowego systemu Windows (i klasycznego modelu COM). Na koniec jest to również kompilowane przy użyciu języka C++/WinRT 1.0.

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

To znowu raczej wątpliwe. Unikatowa własność jest sprzeczna z dzielonym okresem życia MyStringablewewnętrznego licznika odniesień.

Rozwiązanie z językiem C++/WinRT 2.0

W przypadku języka C++/WinRT 2.0 wszystkie te próby bezpośredniego przydzielenia typów implementacji prowadzą do błędu kompilatora. Jest to najlepszy rodzaj błędu i nieskończenie lepszy niż tajemnicza usterka środowiska uruchomieniowego.

Zawsze, gdy musisz wykonać implementację, możesz po prostu użyć winrt::make lub winrt::make_self, jak pokazano powyżej. A teraz, jeśli zapomnisz to zrobić, zostaniesz powitany z błędem kompilatora nawiązującym do tego z odwołaniem do funkcji abstrakcyjnej o nazwie use_make_function_to_create_this_object. To nie jest dokładnie static_assert; ale jest blisko. Mimo to jest to najbardziej niezawodny sposób wykrywania wszystkich opisanych błędów.

Oznacza to, że musimy wprowadzić kilka drobnych ograniczeń dotyczących implementacji. Biorąc pod uwagę, że polegamy na braku przesłonięcia w celu wykrywania bezpośredniej alokacji, szablon funkcji winrt::make musi w pewien sposób spełniać abstrakcyjną funkcję wirtualną z przesłonięciem. Robi to, pochodząc z implementacji z klasą final, która zapewnia nadpisanie. Istnieje kilka rzeczy, które należy obserwować w tym procesie.

Najpierw funkcja wirtualna jest obecna tylko w kompilacjach debugowania. Oznacza to, że wykrywanie nie będzie miało wpływu na rozmiar tabeli wirtualnej w zoptymalizowanych kompilacjach.

Po drugie, ponieważ klasa pochodna używana przez winrt::make to final, oznacza to, że każda dewirtualizacja, którą optymalizator może ewentualnie wyłuskać, będzie miała miejsce nawet wtedy, gdy wcześniej zdecydowano się nie oznaczać swojej klasy implementacji jako final. Więc to poprawa. Odwrotność tego jest taka, że twoja implementacja nie może byćfinal. Jak wcześniej, nie ma to znaczenia, ponieważ wystąpienie typu zawsze będzie final.

Po trzecie, nic nie uniemożliwia oznaczania żadnych funkcji wirtualnych w implementacji jako final. Oczywiście, C++/WinRT różni się bardzo od klasycznego COM i implementacji, takich jak WRL, gdzie wszystko w twojej implementacji ma tendencję do bycia wirtualnym. W języku C++/WinRT wysyłanie wirtualne jest ograniczone do interfejsu binarnego aplikacji (ABI) (zawsze final), a metody implementacji polegają na polimorfizmie statycznym lub polimorfizmie czasu kompilacji. Pozwala to uniknąć niepotrzebnego polimorfizmu środowiska uruchomieniowego, a także oznacza, że jest mało istotny powód stosowania funkcji wirtualnych w implementacji C++/WinRT. Jest to bardzo dobra rzecz i prowadzi do znacznie bardziej przewidywalnego inline'owania.

Po czwarte, ponieważ winrt::make wprowadza klasę pochodną, implementacja nie może mieć prywatnego destruktora. Prywatne destruktory były popularne w klasycznych implementacjach COM, ponieważ wszystko było wirtualne i często zajmowano się bezpośrednio nieprzetworzonymi wskaźnikami, co powodowało łatwość przypadkowego wywołania delete zamiast Release. C++/WinRT stara się maksymalnie utrudnić bezpośrednie operowanie surowymi wskaźnikami. I musisz naprawdę wyjść z drogi, aby uzyskać nieprzetworzone wskaźniki w języku C++/WinRT, które można potencjalnie wywołać delete. Semantyka wartości oznacza, że masz do czynienia z wartościami i odwołaniami; rzadko ze wskaźnikami.

Tym samym, C++/WinRT kwestionuje nasze przekonania o tym, co to znaczy pisać klasyczny kod COM. I to jest całkowicie rozsądne, ponieważ WinRT nie jest klasycznym COM. Klasyczny COM to język asemblera środowiska uruchomieniowego systemu Windows. Nie powinien być to kod, który piszesz każdego dnia. Zamiast tego język C++/WinRT ułatwia pisanie kodu, który jest bardziej podobny do nowoczesnego języka C++, a znacznie mniej jak klasyczny com.

Ważne interfejsy API