Condividi tramite


Diagnosi delle allocazioni dirette

Come spiegato in API Author con C++/WinRT, quando si crea un oggetto di tipo di implementazione, è consigliabile usare la famiglia di helper winrt::make. Questo argomento illustra in modo approfondito una funzionalità C++/WinRT 2.0 che consente di diagnosticare l'errore di allocazione diretta di un oggetto di tipo di implementazione nello stack.

Tali errori possono trasformarsi in misteriosi malfunzionamenti o danneggiamenti difficili e che richiedono tempo per il debug. Quindi questa è una caratteristica importante e vale la pena comprendere lo sfondo.

Impostazione della scena, con MyStringable

Consideriamo prima una semplice implementazione di IStringable.

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

Si supponga ora di dover chiamare una funzione (dall'interno dell'implementazione) che prevede un IStringable come argomento.

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

Il problema è che il tipo MyStringable non è un IStringable.

  • Il nostro tipo MyStringable è un'implementazione dell'interfaccia IStringable.
  • Il tipo IStringable è un tipo proiettato.

Importante

È importante comprendere la distinzione tra un'implementazione di tipo e una proiezione di tipo . Per concetti e termini essenziali, assicurati di leggere Consumare le API con C++/WinRT e Creare API con C++/WinRT.

Lo spazio tra un'implementazione e la proiezione può essere sottile da comprendere. In effetti, per provare a rendere l'implementazione un po' più simile alla proiezione, l'implementazione fornisce conversioni implicite in ognuno dei tipi proiettati che implementa. Questo non significa che possiamo semplicemente farlo.

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

È invece necessario ottenere un riferimento in modo che gli operatori di conversione possano essere usati come candidati per la risoluzione della chiamata.

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

Questo funziona. Una conversione implicita fornisce una conversione (molto efficiente) dal tipo di implementazione al tipo proiettato ed è molto utile per molti scenari. Senza tale struttura, molti tipi di implementazione risulterebbero molto complessi da creare. Se si usa solo il modello di funzione winrt::make (o winrt::make_self) per allocare l'implementazione, tutto è corretto.

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

Potenziali trappole con C++/WinRT 1.0

Tuttavia, le conversioni implicite possono causare problemi. Si consideri questa funzione di supporto inutile.

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

O anche solo questa affermazione apparentemente innocua.

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

Sfortunatamente, il codice come questo ha compilato con C++/WinRT 1.0, a causa di tale conversione implicita. Il problema (molto grave) è che si sta potenzialmente restituendo un tipo proiettato che punta a un oggetto, con conteggio dei riferimenti, la cui memoria di base si trova nello stack temporaneo.

Ecco qualcos'altro compilato con C++/WinRT 1.0.

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

I puntatori raw sono una fonte di bug pericolosi e che richiedono molto lavoro. Non usarli se non è necessario. C++/WinRT si impegna a rendere tutto efficiente senza mai costringerti a usare puntatori grezzi. Ecco qualcos'altro compilato con C++/WinRT 1.0.

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

Questo è un errore su diversi livelli. Abbiamo due diversi conteggi di riferimenti per lo stesso oggetto. Windows Runtime (e prima di esso il COM classico) è basato su un conteggio intrinseco dei riferimenti che non è compatibile con std::shared_ptr. std::shared_ptr ha, naturalmente, molte applicazioni valide; ma non è del tutto necessario quando si condividono oggetti Windows Runtime (e COM classici). Infine, questa operazione viene compilata anche con C++/WinRT 1.0.

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

Questo è di nuovo piuttosto discutibile. La proprietà univoca è in contrasto con il conteggio dei riferimenti intrinseci con durata condivisa di MyStringable.

Soluzione con C++/WinRT 2.0

Con C++/WinRT 2.0, tutti questi tentativi di allocare direttamente i tipi di implementazione generano un errore del compilatore. Questo è il tipo di errore migliore, e infinitamente meglio di un misterioso bug di runtime.

Ogni volta che devi creare un'implementazione, puoi semplicemente usare winrt::make o winrt::make_self, come illustrato in precedenza. A questo punto, se si dimentica di farlo, si riceverà un errore del compilatore alludendo a questa operazione con un riferimento a una funzione astratta denominata use_make_function_to_create_this_object. Non è esattamente un static_assert; ma è vicino. Tuttavia, questo è il modo più affidabile per rilevare tutti gli errori descritti.

Ciò significa che è necessario inserire alcuni vincoli secondari sull'implementazione. Dato che ci si basa sull'assenza di un override per rilevare l'allocazione diretta, il modello di funzione winrt::make deve in qualche modo soddisfare la funzione virtuale astratta con un override. Lo fa derivando da un'implementazione con una classe final che consente di sovrascrivere. Ci sono alcuni aspetti da osservare su questo processo.

In primo luogo, la funzione virtuale è presente solo nelle compilazioni di debug. Ciò significa che il rilevamento non influisce sulle dimensioni della vtable nelle compilazioni ottimizzate.

In secondo luogo, poiché la classe derivata usata da winrt::make è final, significa che qualsiasi devirtualizzazione che l'ottimizzatore può dedurre si verificherà anche se in precedenza si è scelto di non contrassegnare la classe di implementazione come final. Quindi questo è un miglioramento. Al contrario, l'implementazione non può essere final. Anche in questo caso, non ha importanza perché il tipo istanziato sarà sempre final.

In terzo luogo, nulla impedisce di contrassegnare qualsiasi funzione virtuale nell'implementazione come final. Naturalmente, C++/WinRT è molto diverso da COM classico e implementazioni come WRL, dove tutto ciò che riguarda l'implementazione tende a essere virtuale. In C++/WinRT, il dispatch virtuale è limitato all'interfaccia binaria dell'applicazione (ABI) (che è sempre final), e i tuoi metodi di implementazione si basano sul polimorfismo a tempo di compilazione o sul polimorfismo statico. Questo evita il polimorfismo di runtime non necessario e significa anche che c'è poco motivo per le funzioni virtuali nell'implementazione di C++/WinRT. Questo è una cosa molto buona e porta a un inline molto più prevedibile.

Quarto, poiché winrt::make inserisce una classe derivata, l'implementazione non può avere un distruttore privato. I distruttori privati erano popolari nelle implementazioni classiche di COM perché tutto era virtuale, e si lavorava comunemente con puntatori grezzi, rendendo così facile chiamare accidentalmente delete invece di Release. C++/WinRT fa il possibile per rendere difficile la gestione diretta dei puntatori grezzi. E devi davvero uscire dal tuo modo per ottenere un puntatore non elaborato in C++/WinRT che potresti potenzialmente chiamare delete. La semantica dei valori implica trattare con valori e riferimenti e raramente con puntatori.

Quindi, C++/WinRT sfida le nostre nozioni preconcette su cosa significa scrivere codice COM classico. Ed è perfettamente ragionevole perché WinRT non è il COM classico. COM classico è il linguaggio assembly di Windows Runtime. Non dovrebbe essere il codice che scrivi ogni giorno. C++/WinRT ti consente invece di scrivere codice più simile a C++moderno e molto meno simile a COM classico.

API importanti