Modelli - Primi passi

Modelli - Primi passi

L'idea di questo post è abbastanza semplice. Voglio visualizzare i modelli e, in particolare, il processo di creazione di un'istanza del modello. Grazie a C++ Insights, questa visualizzazione è piuttosto semplice.

I modelli (modelli di classe o modelli di funzione) sono famiglie di classi o funzioni. Quando crei un'istanza di un modello, crei una classe concreta o una funzione concreta da queste famiglie di classi o funzioni. Ecco le prime domande semplici, a cui voglio rispondere. Per ragioni di semplicità a volte chiamo un modello di classe una classe generica e un modello di funzione una funzione generica.

Quando dovrei usare un modello?

Dovresti usare un modello quando la tua funzione o classe sta per un'idea così generica, che questa idea non è legata a un tipo concreto. Ad esempio, una funzione come max o un contenitore come vector sono utilizzabili per molti tipi.

Come posso creare un modello?

Presumo che tu abbia implementato una funzione max accettando due int.

int max(int lhs, int rhs) {
 return (lhs > rhs)? lhs : rhs;
}

Creare un modello dalla funzione è in generale semplice.

  1. Inserisci la riga template <typename T> prima della funzione
  2. Sostituisci il tipo di calcestruzzo int con il tipo parametro T .
template <typename T> // (1)
T max(T lhs, T rhs) { // (2)
 return (lhs > rhs)? lhs : rhs;
}

Devo sottolineare due ulteriori osservazioni. Innanzitutto, invece del nome typename , puoi anche utilizzare class . Consiglio vivamente typename , perché T non deve essere una classe ma può essere un tipo, un non tipo o un modello. In secondo luogo, per convenzione, utilizziamo T come nome per il primo parametro di tipo.

La stessa procedura funziona anche quando trasformi una classe in un modello di classe.

Ora arrivo esattamente al punto in cui C++ Insights mi fornisce servizi preziosi.

Cosa succede quando creo un'istanza di un modello?

Istanziare il modello di funzione max per int e double .

template <typename T>
T max(T lhs, T rhs) {
 return (lhs > rhs)? lhs : rhs;
}

int main() {
 
 max(10, 5);
 max(10.5, 5.5);
 
}

C++ Insights fornisce una visione più approfondita di questo processo automatico di creazione di un'istanza del modello:

Il processo di istanziazione del modello crea le righe 6 - 23. Vorrei scrivere alcune parole sull'istanziazione della funzione max per i due int (righe 6 - 13). La riga 6 nello screenshot esprime quella riga 8 nel file sorgente (max(10, 5) ) provoca la generazione delle righe 6 - 13. Presumo che le prime due righe del codice di generazione del compilatore siano le più interessanti.

template<>
int max<int>(int lhs, int rhs)
{
 return (lhs > rhs) ? lhs : rhs;
}

max è un modello di funzione completamente specializzato per int: max<int> . La parte generica è vuota: template<> . Il compilatore viene generato dalla famiglia di max -funziona una funzione concreta per int . Questo significa anche che il compilatore genera una funzione concreta per ogni tipo utilizzato?

Cosa succede quando ho istanziato un modello più di una volta per lo stesso digitare?

Il mio prossimo esempio è basato sui modelli di classe. Ecco un semplice contenitore istanziato due volte per int .

template <typename T, int N>
class Array{
 public:
 int getSize() const{
 return N;
 }
 private:
 T elem[N];
};

int main() {
 
 Array<int, 5> myArr1; // (1)
 Array<int, 10> myArr2; // (2)
 Array<int, 5> myArr3; // (3)
 
}

Ho creato un'istanza due volte Array<int, 5> (riga (1) e (3)) e una volta Array<int, 10> (linea 2). Quando studi l'output di C++ Insights, riconosci che la seconda istanza di Array<int, 5> (riga 3) utilizza la prima istanza già attivata dalla riga (1). Ecco le parti rilevanti dell'output.

Abbiamo finito con questo esempio? No! Ci sono altre due osservazioni interessanti che voglio fare.

Innanzitutto, il processo di creazione di un'istanza del modello è pigro. In secondo luogo, utilizzo un parametro di modello non di tipo.

L'istanza del modello è pigra

Hai riconosciuto che la funzione membro getSize () non è stato istanziato? È disponibile solo la dichiarazione della funzione membro. Il processo di creazione di un'istanza del modello è pigro. Significato, se non è necessario non verrà istanziato. Funziona bene finora che puoi utilizzare codice non valido in una funzione membro. Naturalmente, la funzione membro non deve essere chiamata. Se non mi credi, compila il seguente piccolo programma. Innanzitutto, disabilita la riga (1) e la seconda, abilita la riga (1).

// number.cpp

#include <cmath>
#include <string>

template <typename T>
struct Number {
 int absValue() {
 return std::abs(val);
 }
 T val{};
};

int main() {
 
 Number<std::string> numb;
 // numb.absValue(); // (1)
 
}

Torniamo al mio programma precedente e invochiamo getSize() . Ecco il main modificato programma.

int main() {
 
 Array<int, 5> myArr1; 
 Array<int, 10> myArr2; 
 Array<int, 5> myArr3; 
 myArr3.getSize(); // (1)
 
}

Di conseguenza, lo screenshot seguente mostra il codice generato dal compilatore per la funzione membro getSize() (righe 18 - 21).

int come parametro di modello non di tipo

In questo esempio ho usato due parametri di tipo nel secondo è, in particolare, un int. int è un esempio di parametro di modello non di tipo. Oltre a int , puoi usare tutti i tipi integrali, i tipi a virgola mobile (C++20), ma anche i puntatori o i riferimenti come parametri del modello non di tipo. Cosa succede quando ho istanziato due array di lunghezze diverse?

template <typename T, int N>
class Array{
 public:
 int getSize() const{
 return N;
 }
 private:
 T elem[N];
};

int main() {
 
 Array<float, 5> myArr1;
 Array<float, 10> myArr2;
 
}

Probabilmente hai indovinato. Vengono istanziate due matrici. Ecco l'output cruciale di C++ Insights

Ciò significa che entrambe le istanze utilizzano int diversi i valori creano tipi diversi.

Cosa c'è dopo

Dopo questi primi passaggi con i modelli, nel prossimo post farò un tuffo nei modelli di funzioni.