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.