Diagnosi delle allocazioni dirette

Come illustrato in Creare API con C++/WinRT, quando crei un oggetto di tipo di implementazione, devi usare la famiglia di helper winrt::make per farlo. 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 arresti anomali misteriosi o corruzioni dei dati difficili da individuare e correggere, che richiedono molto tempo per il debug. Quindi questa è una caratteristica importante e vale la pena comprendere lo sfondo.

Impostazione della scena con MyStringable

Prima di tutto, si consideri 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 MyStringablenon è un IStringable.

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

Importante

È importante comprendere la distinzione tra un tipo di implementazione e un tipo proiettato. Per i concetti e i termini essenziali, leggere Usare 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 insidie con C++/WinRT 1.0

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

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

O anche solo questa affermazione apparentemente innocua.

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

Sfortunatamente, il codice come quello è stato compilato con C++/WinRT 1.0, a causa di tale conversione implicita. Il problema (molto grave) è che potremmo restituire un tipo proiettato che punta a un oggetto a conteggio di riferimenti la cui memoria sottostante si trova nello stack effimero.

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

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

I puntatori grezzi sono pericolosi e rappresentano una fonte di bug che richiede molto lavoro. Non usarli se non è necessario. C++/WinRT fa di tutto per 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. Sono disponibili due diversi conteggi dei riferimenti per lo stesso oggetto. Windows Runtime (e, prima di esso, il COM classico) si basa su un meccanismo intrinseco di conteggio 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 Windows Runtime (e gli oggetti 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 opposizione alla durata condivisa del conteggio dei riferimenti intrinseci 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. E ora, se si dimentica di farlo, si riceverà un errore del compilatore alludendo a questo con un riferimento a una funzione astratta denominata use_make_function_to_create_this_object. Non è esattamente un static_assert; ma ci va 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 dall'implementazione con una final classe che fornisce l'override. 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 tabella virtuale nelle compilazioni ottimizzate.

In secondo luogo, poiché la classe derivata che winrt::make uses è final, significa che qualsiasi devirtualizzazione che l'ottimizzatore può dedurre può verificarsi anche se in precedenza hai scelto di non contrassegnare la classe di implementazione come final. Quindi questo è un miglioramento. Il contrario è che l'implementazione non può essere final. Anche in questo caso, ciò non ha alcuna 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 ABI (Application Binary Interface) (che è sempre final) e i metodi di implementazione si basano sul polimorfismo statico o in fase di compilazione. Questo evita il polimorfismo di runtime non necessario e significa anche che c'è poco motivo per le funzioni virtuali nell'implementazione di C++/WinRT. Il che è un'ottima cosa e rende l'inlining assai più prevedibile.

Quarto, poiché winrt::make inserisce una classe derivata, l'implementazione non può avere un distruttore privato. I distruttori privati erano diffusi nelle implementazioni COM classiche perché, come detto, tutto era virtuale ed era comune lavorare direttamente con puntatori raw, quindi era facile chiamare accidentalmente delete invece di Release. C++/WinRT fa di tutto per rendere difficile gestire direttamente i puntatori raw. E dovresti fare davvero i salti mortali per ottenere un puntatore raw in C++/WinRT su cui potresti potenzialmente chiamare delete. La semantica dei valori significa che si tratta di 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 classico COM. Il COM classico è il linguaggio assembly di Windows Runtime. Non deve essere il codice scritto ogni giorno. C++/WinRT ti consente invece di scrivere codice più simile a C++moderno e molto meno simile a COM classico.

API importanti