Share via


Il presente articolo è stato tradotto automaticamente.

Windows con C++

Aggiunta di Compile-Time tipo controllando per Printf

Kenny Kerr

Kenny KerrHo esplorato alcune tecniche per rendere più comodo da usare con C++ moderno nella mia colonna marzo 2015 printf (msdn.microsoft.com/magazine/dn913181). Ho mostrato come trasformare gli argomenti utilizzando un modello di variadic al fine di colmare il divario tra la classe string C++ ufficiale e la funzione printf antiquati. Perché preoccuparsi? Beh, printf è molto veloce, e una soluzione per l'output che può sfruttare che pur consentendo agli sviluppatori di scrivere più sicura, codice di livello superiore è certamente auspicabile formattato. Il risultato fu un modello di funzione variadic Print che potrebbe prendere un programma semplice come questo:

int main()
{
  std::string const hello = "Hello";
  std::wstring const world = L"World";
  Print("%d %s %ls\n", 123, hello, world);
}

Ed espandere efficacemente la funzione printf interiore come segue:

printf("%d %s %ls\n", Argument(123), Argument(hello), Argument(world));

Il modello di funzione argomento sarebbe poi anche compilare via e lasciare le funzioni di accesso necessari:

printf("%d %s %ls\n", 123, hello.c_str(), world.c_str());

Mentre questo è un comodo ponte tra C++ moderno e la funzione printf tradizionale, non fa nulla per risolvere le difficoltà di scrittura di codice corretto con printf. printf è ancora printf e mi affido il compilatore non interamente onnisciente e biblioteca per fiutare eventuali incongruenze tra gli identificatori di conversione e gli argomenti effettivi forniti dal chiamante.

C++ sicuramente moderno può fare di meglio. Molti sviluppatori hanno tentato di fare di meglio. Il guaio è che gli sviluppatori differenti hanno differenti esigenze o priorità. Alcuni sono felici di prendere un colpo di piccole prestazioni e semplicemente affidamento su < iostream > per il controllo del tipo e l'estensibilità. Gli altri hanno ideato elaborati librerie che offrono caratteristiche interessanti che richiedono ulteriori strutture e dotazioni per tenere traccia dello stato di formattazione. Personalmente, non sono soddisfatto con soluzioni che introducono prestazioni e sovraccarico di runtime per quello che dovrebbe essere un'operazione fondamentale e veloce. Se è piccolo e veloce in C, dovrebbe essere piccolo, veloce e facile da usare correttamente in C++. "Più lento" non dovrebbe entrare in quell'equazione.

Così cosa si può fare? Beh, un po' astrazione è in ordine, purché le astrazioni possono essere compilate via per lasciare qualcosa di molto vicino a un'istruzione printf manoscritta. La chiave è capire che identificatori di conversione quali %d e %s sono davvero solo segnaposti per le argomenti o i valori che seguono. Il guaio è che questi segnaposto fare ipotesi circa i tipi degli argomenti corrispondenti senza avere alcun modo di sapere se quei tipi sono corretti. Piuttosto che cercare di aggiungere il controllo di tipo runtime che sarà confermare queste ipotesi, facciamo buttare via queste informazioni di pseudo- tipo e invece lasciare che il compilatore di dedurre i tipi degli argomenti come essi stanno naturalmente risolto. Così piuttosto che di scrittura:

printf("%s %d\n", "Hello", 2015);

Io invece Devo limitare la stringa di formato ai personaggi reali di uscita e tutti i segnaposto di espandere. Potrei anche usare lo stesso metacarattere come segnaposto:

Write("% %\n", "Hello", 2015);

Non c'è davvero nessun meno informazioni di tipo in questa versione rispetto nell'esempio precedente printf. L'unico motivo printf necessari per incorporare che informazioni di tipo extra erano perché il linguaggio di programmazione C mancava variadic templates. Quest'ultimo è anche molto più pulito. Certamente sarei felice se non ho dovuto tenere cercando vari identificatori di formato printf per assicurarsi che ho appena a destra.

Anche io non voglio limitare l'output solo sulla console. Che ne dici di formattazione in una stringa? Riguardo a qualche altra destinazione? Una delle sfide di funzionamento con printf è che mentre supporta uscita alle varie destinazioni, lo fa attraverso funzioni distinte che non sono sovraccarichi e sono difficili da utilizzare genericamente. Per essere onesti, il linguaggio di programmazione C non supporta l'overload o programmazione generica. Ancora, diciamo non ripetere la storia. Vorrei essere in grado di stampare la console appena come facilmente e genericamente come posso stampare una stringa o un file. Quello che ho in mente è illustrata Figura 1.

Figura 1 Output generico con deduzione del tipo

Write(printf, "Hello %\n", 2015);
FILE * file = // Open file
Write(file, "Hello %\n", 2015);
std::string text;
Write(text, "Hello %\n", 2015);

Da una prospettiva di progettazione o di esecuzione, dovrebbe esserci uno modello di funzione di scrittura che agisce come un driver e qualsiasi numero di bersagli o le schede di destinazione associati genericamente basato sul bersaglio identificato dal chiamante. Uno sviluppatore dovrebbe quindi essere in grado di aggiungere facilmente altre destinazioni come necessario. Così come questo lavoro? Una possibilità è fare il bersaglio un parametro di template. Qualcosa come questo:

template <typename Target, typename ... Args>
void Write(Target & target,
  char const * const format, Args const & ... args)
{
  target(format, args ...);
}
int main()
{
  Write(printf, "Hello %d\n", 2015);
}

Questo funziona per un punto. Posso scrivere altri obiettivi che sono conformi alla firma preveduta di printf e dovrebbe funzionare abbastanza bene. Potrei scrivere un oggetto funzione che è conforme e aggiunge l'output di una stringa:

struct StringAdapter
{
  std::string & Target;
  template <typename ... Args>
  void operator()(char const * const format, Args const & ... args)
  {
    // Append text
  }
};

Quindi posso usarlo con lo stesso modello di funzione di scrittura:

std::string target;
Write(StringAdapter{ target }, "Hello %d", 2015);
assert(target == "Hello 2015");

In un primo momento questo potrebbe sembrare abbastanza elegante e anche flessibile. Riesco a scrivere tutti i tipi di adattatore funzioni o oggetti funzione. Ma in pratica rapidamente diventa piuttosto noioso. Sarebbe molto più opportuno semplicemente passare la stringa come destinazione direttamente e hanno il modello di funzione di scrittura a prendersi cura di adattandola in base al suo tipo. Quindi, diamo il modello di funzione scrittura corrisponde l'obiettivo richiesto con un paio di funzioni in overload per accodare o formattazione dell'output. Un piccolo riferimento indiretto compile-time va un senso lungo. Rendendosi conto che gran parte della produzione che possa dover essere scritti sarà semplicemente essere testo senza tutti i segnaposto, aggiungerò non uno, ma un paio di overload. Il primo sarebbe semplicemente aggiungere testo. Ecco una funzione Append per le stringhe:

void Append(std::string & target,
  char const * const value, size_t const size)
{
  target.append(value, size);
}

Posso quindi fornire un overload di Append per printf:

template <typename P>
void Append(P target, char const * const value, size_t const size)
{
  target("%.*s", size, value);
}

Potrei hanno evitato il modello utilizzando un puntatore a funzione printf firma di corrispondenza, ma questo è un po' più flessibile, perché altre funzioni in teoria potrebbero essere associati a tale implementazione e il compilatore non è ostacolato in alcun modo dall'indirezione del puntatore. Posso fornire anche un sovraccarico per l'output di file o un flusso:

void Append(FILE * target,
  char const * const value, size_t const size)
{
  fprintf(target, "%.*s", size, value);
}

Naturalmente, l'output formattato è ancora indispensabile. Ecco una funzione AppendFormat per le stringhe:

template <typename ... Args>
void AppendFormat(std::string & target,
  char const * const format, Args ... args)
{
  int const back = target.size();
  int const size = snprintf(nullptr, 0, format, args ...);
  target.resize(back + size);
  snprintf(&target[back], size + 1, format, args ...);
}

Essa determina innanzitutto quanto spazio supplementare è necessaria prima di ridimensionare il bersaglio e direttamente nella stringa di formattazione del testo. Si è tentati di cercare di evitare di chiamare snprintf due volte controllando se c'è abbastanza spazio nel buffer. Il motivo che tendo sempre chiamare snprintf due volte è perché test informale ha indicato che due volte la chiamata è solitamente più economica di ridimensionamento per capacità. Anche se un'allocazione non è obbligatorio, quei caratteri extra sono azzerate, che tende ad essere più costosi. Tuttavia, questo è molto soggettivo, dipendente da modelli di dati e quanto frequentemente viene riutilizzata la stringa di destinazione. Ed ecco uno per printf:

template <typename P, typename ... Args>
void AppendFormat(P target, char const * const format, Args ... args)
{
  target(format, args ...);
}

Un sovraccarico per il file di output è proprio così semplice:

template <typename ... Args>
void AppendFormat(FILE * target,
  char const * const format, Args ... args)
{
  fprintf(target, format, args ...);
}

Ora ho uno dei blocchetti di costruzione in luogo per la funzione di autista di scrittura. Il blocco di costruzione altri necessario è un modo generico di gestione argomento formattazione. Mentre la tecnica che illustrato nel mio articolo di marzo 2015 era semplice ed elegante, mancava la capacità di affrontare eventuali valori non mappate direttamente ai tipi supportati da printf. Non poteva anche trattare con argomento espansione o i valori degli argomenti complessi quali tipi definiti dall'utente. Ancora una volta, un set di funzioni in overload può risolvere il problema abbastanza elegantemente. Supponiamo che la funzione di autista di scrittura passerà ogni argomento a una funzione di WriteArgument. Ecco uno per le stringhe:

template <typename Target>
void WriteArgument(Target & target, std::string const & value)
{
  Append(target, value.c_str(), value.size());
}

Le varie funzioni di WriteArgument sempre accetta due argomenti. Il primo rappresenta il target generico mentre il secondo è l'argomento specifico per scrivere. Qui, sto facendo affidamento sull'esistenza di una funzione Append per abbinare il bersaglio e prendersi cura di aggiungere il valore alla fine del bersaglio. La funzione WriteArgument non ha bisogno di sapere che cosa è in realtà quell'obiettivo. In teoria potrei evitare le funzioni di adattatore di destinazione, ma che si tradurrebbe in una crescita quadratica in overload WriteArgument. Ecco un'altra funzione di WriteArgument per gli argomenti interi:

template <typename Target>
void WriteArgument(Target & target, int const value)
{
  AppendFormat(target, "%d", value);
}

In questo caso, la funzione di WriteArgument prevede una funzione AppendFormat per abbinare il bersaglio. Come con gli overload Append e AppendFormat, scrivendo WriteArgument ulteriori funzioni è semplice. La bellezza di questo approccio è che le schede di argomento non necessario restituire qualche valore dello stack della funzione printf, come hanno fatto nella versione marzo 2015. Invece, gli overload WriteArgument ambito effettivamente l'output tale che il bersaglio è scritto immediatamente. Questo significa tipi complessi possono essere utilizzati come argomenti e deposito temporaneo può anche essere invocata per formattare la loro rappresentazione del testo. Qui è un sovraccarico di WriteArgument per GUID:

template <typename Target>
void WriteArgument(Target & target, GUID const & value)
{
  wchar_t buffer[39];
  StringFromGUID2(value, buffer, _countof(buffer));
  AppendFormat(target, "%.*ls", 36, buffer + 1);
}

Potrei anche sostituire la funzione di Windows StringFromGUID2 e formattarlo direttamente, forse per migliorare le prestazioni oppure aggiungere portabilità, ma questo dimostra chiaramente la potenza e la flessibilità di questo approccio. Tipi definiti dall'utente possono facilmente essere supportati con l'aggiunta di un sovraccarico di WriteArgument. Ho chiamato li sovraccarichi qui, ma a rigor di termini non devono essere. La libreria di output può certamente fornire un set di overload per obiettivi e argomenti comuni, ma la funzione di autista di scrittura non dovrebbe assumere le funzioni di adattatore sono sovraccarichi e, invece, dovrebbero trattarli come non il membro inizio e fine funzioni definite dalla libreria Standard di C++. Non il membro iniziano e fine funzioni sono estendibile e adattabile a tutti i tipi di contenitori standard e non standard, proprio perché essi non hanno bisogno di risiedere nel namespace std, ma dovrebbe invece essere locale per lo spazio dei nomi del tipo essere abbinato. Allo stesso modo, queste funzioni di adattatore di destinazione e l'argomento dovrebbero essere in grado di risiedere in altri spazi dei nomi per supportare gli sviluppatori obiettivi e argomenti definiti dall'utente. Così che cosa fa apparire la funzione scrittura del driver come? Per cominciare, c'è solo una funzione di scrittura:

template <typename Target, unsigned Count, typename ... Args>
void Write(Target & target,
  char const (&format)[Count], Args const & ... args)
{
  assert(Internal::CountPlaceholders(format) == sizeof ... (args));
  Internal::Write(target, format, Count - 1, args ...);
}

La prima cosa che deve fare è determinare se il numero di segnaposto nella stringa di formato è uguale al numero di argomenti in pack variadic parametro. Qui, sto usando un'asserzione di runtime, ma questo in realtà dovrebbe essere un static_assert che controlla la stringa di formato in fase di compilazione. Purtroppo, Visual C++ non abbastanza c'è ancora. Ancora, posso scrivere il codice così che quando il compilatore di catture, il codice può essere facilmente aggiornato per verificare la stringa di formato in fase di compilazione. Come tale, la funzione interna di CountPlaceholders dovrebbe essere un constexpr:

constexpr unsigned CountPlaceholders(char const * const format)
{
  return (*format == '%') +
    (*format == '\0' ? 0 : CountPlaceholders(format + 1));
}

Quando Visual C++ raggiunge la piena conformità con C + + 14, almeno per quanto riguarda constexpr, dovrebbe essere in grado di sostituire semplicemente l'asserzione all'interno della funzione di scrittura con static_assert. Allora è alla funzione interna overload Write a stendere l'output argomento specifico in fase di compilazione. Qui, posso affidarsi al compilatore di generare e chiamare necessarie overload della funzione scrittura interna per soddisfare il pack di parametro variadic espanso:

template <typename Target, typename First, typename ... Rest>
void Write(Target & target, char const * const value,
  size_t const size, First const & first, Rest const & ... rest)
{
  // Magic goes here
}

Mi concentrerò su quella magia in un attimo. In definitiva, il compilatore a corto di argomenti e un sovraccarico di variadic non sarà richiesto di completare l'operazione:

template <typename Target>
void Write(Target & target, char const * const value, size_t const size)
{
  Append(target, value, size);
}

Entrambe le funzioni di scrittura interne accettano un valore, insieme con la dimensione del valore. Il modello di funzione variadic scrittura deve assumere ulteriormente c'è almeno un segnaposto nel valore. La funzione Write variadic non serve fare nessuna tale presupposto e può utilizzare semplicemente la funzione Append generica scrivere qualsiasi parte finale della stringa di formato. Prima che la funzione Write variadic può scrivere gli argomenti, deve prima scrivere i caratteri principali e, naturalmente, trovare il primo segnaposto o metacarattere:

size_t placeholder = 0;
while (value[placeholder] != '%')
{
  ++placeholder;
}

Solo allora possono scrivere i caratteri iniziali:

assert(value[placeholder] == '%');
Append(target, value, placeholder);

Il primo argomento può essere quindi scritto e il processo si ripete fino a senza ulteriori argomenti e segnaposti sono di sinistra:

WriteArgument(target, first);
Write(target, value + placeholder + 1, size - placeholder - 1, rest ...);

Ora posso sostenere l'output generico in Figura 1. Posso anche convertire un GUID in una stringa molto semplicemente:

std::string text;
Write(text, "{%}", __uuidof(IUnknown));
assert(text == "{00000000-0000-0000-C000-000000000046}");

Riguardo a qualcosa di un po ' più interessante? Come visualizzare un vettore:

std::vector<int> const numbers{ 1, 2, 3, 4, 5, 6 };
std::string text;
Write(text, "{ % }", numbers);
assert(text == "{ 1, 2, 3, 4, 5, 6 }");

Per questo devo semplicemente scrivere un template di funzione WriteArgument che accetta un vettore come argomento, come mostrato Figura 2.

Figura 2 visualizzazione di un vettore

template <typename Target, typename Value>
void WriteArgument(Target & target, std::vector<Value> const & values)
{
  for (size_t i = 0; i != values.size(); ++i)
  {
    if (i != 0)
    {
      WriteArgument(target, ", ");
    }
    WriteArgument(target, values[i]);
  }
}

Notare come io non forzare il tipo degli elementi nel vettore. Ciò significa che ora posso usare la stessa implementazione di visualizzare un vettore di stringhe:

std::vector<std::string> const names{ "Jim", "Jane", "June" };
std::string text;
Write(text, "{ % }", names);
assert(text == "{ Jim, Jane, June }");

Naturalmente, che pone la domanda: Cosa succede se voglio ampliare ulteriormente un segnaposto? Certo, posso scrivere un WriteArgument per un contenitore, ma non fornisce alcuna flessibilità nel tweaking l'output. Immaginate che devo definire una tavolozza coloristica di un'app e ho i colori primari e colori secondari:

std::vector<std::string> const primary = { "Red", "Green", "Blue" };
std::vector<std::string> const secondary = { "Cyan", "Yellow" };

La funzione Write volentieri questo formato per me:

Write(printf,
  "<Colors>%%</Colors>",
  primary,
  secondary);

L'uscita, tuttavia, non è abbastanza quello che voglio:

<Colors>Red, Green, BlueCyan, Yellow</Colors>

Questo è ovviamente sbagliato. Invece, vorrei segnare i colori tale che so che sono primarie e che sono secondari. Forse qualcosa di simile:

<Colors>
  <Primary>Red</Primary>
  <Primary>Green</Primary>
  <Primary>Blue</Primary>
  <Secondary>Cyan</Secondary>
  <Secondary>Yellow</Secondary>
</Colors>

Aggiungiamo una funzione WriteArgument più che può fornire questo livello di estensibilità:

template <typename Target, typename Arg>
void WriteArgument(Target & target, Arg const & value)
{
  value(target);
}

Si noti che l'operazione sembra essere capovolta sulla sua testa. Piuttosto che passando il valore di destinazione, la destinazione viene passata il valore. In questo modo, posso fornire una funzione associata come un argu­mento invece solo un valore. Posso allegare alcuni comportamenti definiti dall'utente e non semplicemente un valore definito dall'utente. Ecco una funzione WriteColors che fa quello che voglio:

void WriteColors(int (*target)(char const *, ...),
  std::vector<std::string> const & colors, std::string const & tag)
{
  for (std::string const & color : colors)
  {
    Write(target, "<%>%</%>", tag, color, tag);
  }
}

Notare che questo non è un modello di funzione e ho dovuto essenzialmente hardcode esso per un singolo bersaglio. Questa è una personalizzazione specifica destinazione, ma dimostra ciò che è possibile anche quando devi uscire la deduzione del tipo generico fornita direttamente dalla funzione scrittura driver. Ma come può essere incorporato in un più grande scrittura oper­aggio? Beh, si potrebbe essere tentati per un momento di scrivere questo:

Write(printf,
  "<Colors>\n%%</Colors>\n",
  WriteColors(printf, primary, "Primary"),
  WriteColors(printf, secondary, "Secondary"));

Mettendo da parte per un momento il fatto che questo non verrà compilato, si davvero non darvi la giusta sequenza di eventi, comunque. Se questo dovesse funzionare, i colori sarebbe stampati prima dell'apertura < colori > etichetta. Chiaramente, essi dovrebbe chiamarsi come se fossero argomenti nell'ordine in cui appaiono. E questo è ciò che permette il nuovo modello di funzione WriteArgument. Ho solo bisogno di associare le invocazioni di WriteColors tale che possono essere chiamati in una fase successiva. Per che rendere ancora più semplice per qualcuno utilizzando la funzione di autista di scrittura, posso offrire fino un wrapper utile impegnare:

template <typename F, typename ... Args>
auto Bind(F call, Args && ... args)
{
  return std::bind(call, std::placeholders::_1,
    std::forward<Args>(args) ...);
}

Questo modello di funzione Bind assicura semplicemente che un segnaposto è riservato per la destinazione finale in cui verrà scritto. Io posso poi giustamente formato mia tavolozza di colori come segue:

Write(printf,
  "<Colors>%%</Colors>\n",
  Bind(WriteColors, std::ref(primary), "Primary"),
  Bind(WriteColors, std::ref(secondary), "Secondary"));

E ottenere i risultati attesi e contrassegnati. Le funzioni di supporto del Rif non sono strettamente necessarie, ma evitare di fare una copia dei contenitori per i wrapper di chiamata.

Non convinto? Le possibilità sono infinite. È possibile gestire gli argomenti della stringa carattere efficientemente per ampio e normali caratteri:

template <typename Target, unsigned Count>
void WriteArgument(Target & target, char const (&value)[Count])
{
  Append(target, value, Count - 1);
}
template <typename Target, unsigned Count>
void WriteArgument(Target & target, wchar_t const (&value)[Count])
{
  AppendFormat(target, "%.*ls", Count - 1, value);
}

In questo modo che posso tranquillamente e facilmente scrivere output utilizzando caratteri differenti imposta:

Write(printf, "% %", "Hello", L"World");

Cosa succede se non specificamente o inizialmente necessario scrivere output, ma invece devi solo calcolare quanto spazio sarebbero necessario? Nessun problema, posso semplicemente creare un nuovo target che riassume:

void Append(size_t & target, char const *, size_t const size)
{
  target += size;
}
template <typename ... Args>
void AppendFormat(size_t & target,
  char const * const format, Args ... args)
{
  target += snprintf(nullptr, 0, format, args ...);
}

Ora posso calcolare la taglia desiderata semplicemente:

size_t count = 0;
Write(count, "Hello %", 2015);
assert(count == strlen("Hello 2015"));

Penso che sia sicuro dire che questa soluzione affronta infine la fragilità di tipo inerente usando printf direttamente pur conservando la maggior parte dei benefici delle prestazioni. C++ moderno è più in grado di soddisfare le esigenze degli sviluppatori alla ricerca di un ambiente produttivo con controllo mantenendo le prestazioni per le quali C e C++ è tradizionalmente conosciuta di tipo affidabile.


Kenny Kerr è un programmatore di computer basato in Canada, così come un autore per Pluralsight e MVP Microsoft. Ha Blog a kennykerr.ca e si può seguirlo su Twitter a twitter.com/kennykerr.

Grazie al seguente Microsoft esperto tecnico per la revisione di questo articolo: James McNellis