Diagnosi delle allocazioni dirette

Come illustrato in Creare API con C++/WinRT, quando crei un oggetto di tipo implementazione, devi usare la famiglia di helper winrt::make. Questo argomento illustra in dettaglio una funzionalità di C++/WinRT 2.0 che ti aiuta a diagnosticare l'errore relativo all'allocazione diretta di un oggetto di tipo implementazione nello stack.

Questi errori possono portare ad arresti anomali o danneggiamenti misteriosi, il cui debug risulta difficile e dispendioso in termini di tempo. Si tratta quindi di una funzionalità importante, di cui è opportuno comprendere le basi.

MyStringable per iniziare

Considera innanzitutto una semplice implementazione di IStringable.

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

Ora immagina di dover chiamare una funzione (dall'implementazione) che prevede un elemento IStringable come argomento.

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

Il problema è rappresentato dal fatto che il tipo MyStringable dell'esempio non è un elemento IStringable.

  • Il tipo MyStringable dell'esempio è 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 fondamentali, leggi Utilizzare API con C++/WinRT e Creare API con C++/WinRT.

La differenza tra un'implementazione e la proiezione può essere difficile da cogliere. Anzi, nel tentativo di risultare più simile alla proiezione, l'implementazione fornisce conversioni implicite a ognuno dei tipi proiettati che implementa. Questo non significa che si possa eseguire direttamente tale operazione.

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 risolvere la chiamata.

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

In questo modo funziona tutto. Una conversione implicita fornisce una conversione (molto efficiente) dal tipo di implementazione al tipo proiettato e questo aspetto è molto utile in molti scenari. Senza di essa, sarebbe molto difficile creare diversi tipi di implementazione. Se allochi l'implementazione usando solo il modello di funzione winrt::make (o winrt::make_self), non si verificano problemi.

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

Potenziali rischi correlati all'uso di C++/WinRT 1.0

Le conversioni implicite possono però creare problemi. Considera questa inutile funzione helper.

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

Oppure questa istruzione apparentemente innocua.

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

Sfortunatamente questo codice è stato compilato con C++/WinRT 1.0 a causa di tale conversione implicita. Il problema (molto serio) è dato dal fatto che potenzialmente viene restituito un tipo proiettato che punta a un oggetto con conteggio dei riferimenti la cui memoria di supporto si trova nello stack temporaneo.

Di seguito è riportato altro codice che è stato compilato con C++/WinRT 1.0.

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

I puntatori non elaborati sono una fonte di bug pericolosa e impegnativa. Non usarli se non è necessario. C++/WinRT tenta di rendere tutto efficiente senza obbligarti mai a usare puntatori non elaborati. Di seguito è riportato altro codice che è stato compilato con C++/WinRT 1.0.

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

Questo è un errore a diversi livelli. Per lo stesso oggetto sono presenti due diversi conteggi dei riferimenti. Windows Runtime (e prima di esso COM classico) è basato su un conteggio dei riferimenti intrinseco che non è compatibile con std::shared_ptr. std::shared_ptr ha ovviamente molte applicazioni valide, ma è assolutamente inutile quando condividi oggetti di Windows Runtime (e di COM classico). Con C++/WinRT 1.0 è stato compilato anche il codice seguente.

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

Questa è un'altra situazione piuttosto dubbia. La proprietà univoca è in contrapposizione con la durata condivisa del conteggio intrinseco dei riferimenti di MyStringable.

Soluzione con C++/WinRT 2.0

Con C++/WinRT 2.0 tutti questi tentativi di allocare direttamente i tipi di implementazione portano a un errore del compilatore. Si tratta del miglior tipo di errore che possa essere generato, decisamente più gestibile rispetto a un misterioso bug di runtime.

Ogni volta che devi effettuare un'implementazione, puoi usare semplicemente winrt::make o winrt::make_self, come illustrato in precedenza. E ora, se ti dimentichi di procedere in questo modo, riceverai un errore del compilatore che allude a tale condizione con un riferimento a una funzione astratta denominata use_make_function_to_create_this_object. Anche se non corrisponde esattamente a un elemento static_assert, è molto simile. Questo è in ogni caso il modo più affidabile per rilevare tutti gli errori descritti.

È quindi necessario porre alcune piccole limitazioni relative all'implementazione. Poiché ci si basa sull'assenza di un override per rilevare l'allocazione diretta, il modello di funzione winrt::make deve soddisfare in qualche modo la funzione virtuale astratta con un override. A tale scopo, deriva dall'implementazione con una classe final che fornisce l'override. Relativamente a questo processo, è necessario osservare alcuni aspetti.

In primo luogo, la funzione virtuale è presente solo nelle build di debug. Il rilevamento pertanto non influirà sulle dimensioni dell'elemento vtable nelle build ottimizzate.

In secondo luogo, poiché la classe derivata usata da winrt::make è final, qualsiasi devirtualizzazione che l'ottimizzatore può eventualmente dedurre verrà eseguita anche se in precedenza hai scelto di non contrassegnare la classe di implementazione come final. Si tratta quindi di un miglioramento. Il contrario è che l'implementazione non può essere final. Anche in questo caso non ci saranno conseguenze perché il tipo di cui è stata creata un'istanza sarà sempre final.

In terzo luogo, niente può impedirti di contrassegnare eventuali funzioni virtuali nell'implementazione come final. C++/WinRT ovviamente è molto diverso da COM classico e dalle implementazioni come WRL, in cui ogni elemento relativo all'implementazione tende a essere virtuale. In C++/WinRT l'invio virtuale è limitato all'interfaccia ABI (Application Binary Interface), che è sempre final, e i metodi di implementazione si basano sul polimorfismo statico o della fase di compilazione. In questo modo si evita il polimorfismo di runtime non necessario ed è raro che si debbano inserire funzioni virtuali nell'implementazione C++/WinRT. Si tratta di un aspetto molto positivo, che rende decisamente più prevedibile l'inlining.

In quarto luogo, poiché winrt::make inserisce una classe derivata, l'implementazione non può avere un distruttore privato. I distruttori privati erano molto diffusi nelle implementazioni di COM classico perché, anche in questo caso, tutti gli elementi erano virtuali ed era pratica comune gestire direttamente i puntatori non elaborati. Era quindi facile chiamare accidentalmente delete anziché Release. C++/WinRT cerca di metterti in condizione di non gestire direttamente i puntatori non elaborati. Dovresti quindi cercare in tutti i modi di ottenere in C++/WinRT un puntatore non elaborato su cui chiamare potenzialmente delete. La semantica dei valori significa che stai gestendo valori e riferimenti e raramente i puntatori.

C++/WinRT pertanto va contro le nozioni preconcette relative a cosa significhi scrivere codice di COM classico. Questo è assolutamente ragionevole perché WinRT non è COM classico. COM classico è il linguaggio assembly di Windows Runtime. Non dovrebbe perciò costituire il codice che scrivi ogni giorno. C++/WinRT ti porta invece a scrivere codice più simile a C++ moderno e molto più distante da COM classico.

API importanti