Condividi tramite


Modelli (C++)

I modelli sono la base per la programmazione generica in C++. Come linguaggio fortemente tipizzato, C++ richiede che tutte le variabili abbiano un tipo specifico, dichiarato in modo esplicito dal programmatore o dedotto dal compilatore. Tuttavia, molte strutture di dati e algoritmi hanno lo stesso aspetto indipendentemente dal tipo su cui operano. I modelli consentono di definire le operazioni di una classe o di una funzione e consentire all'utente di specificare i tipi concreti su cui devono funzionare tali operazioni.

Definizione e uso di modelli

Un modello è un costrutto che genera un tipo o una funzione normale in fase di compilazione in base agli argomenti forniti dall'utente per i parametri del modello. Ad esempio, è possibile definire un modello di funzione simile al seguente:

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

Il codice precedente descrive un modello per una funzione generica con un singolo parametro di tipo T, il cui valore restituito e i parametri di chiamata (lhs e rhs) sono tutti di questo tipo. È possibile assegnare un nome a un parametro di tipo qualsiasi elemento desiderato, ma per convenzione vengono usate più comunemente singole lettere maiuscole. T è un parametro di modello. La typename parola chiave indica che questo parametro è un segnaposto per un tipo. Quando viene chiamata la funzione , il compilatore sostituirà ogni istanza di T con l'argomento di tipo concreto specificato dall'utente o dedotto dal compilatore. Il processo in cui il compilatore genera una classe o una funzione da un modello viene definita creazione di istanze del modello. minimum<int> È una creazione di istanze del modello minimum<T>.

Altrove, un utente può dichiarare un'istanza del modello specializzata per int. Si supponga che get_a() e get_b() siano funzioni che restituiscono un valore int:

int a = get_a();
int b = get_b();
int i = minimum<int>(a, b);

Tuttavia, poiché si tratta di un modello di funzione e il compilatore può dedurre il tipo di T dagli argomenti a e b, è possibile chiamarlo esattamente come una funzione normale:

int i = minimum(a, b);

Quando il compilatore rileva l'ultima istruzione, genera una nuova funzione in cui ogni occorrenza di T nel modello viene sostituita con int:

int minimum(const int& lhs, const int& rhs)
{
    return lhs < rhs ? lhs : rhs;
}

Le regole per il modo in cui il compilatore esegue la deduzione dei tipi nei modelli di funzione sono basate sulle regole per le funzioni normali. Per altre informazioni, vedere Risoluzione dell'overload delle chiamate al modello di funzione.

Parametri di tipo

minimum Nel modello precedente si noti che il parametro di tipo T non è qualificato in alcun modo fino a quando non viene usato nei parametri della chiamata di funzione, in cui vengono aggiunti i qualificatori const e reference.

Non esiste alcun limite pratico al numero di parametri di tipo. Separare più parametri in base a virgole:

template <typename T, typename U, typename V> class Foo{};

La parola chiave class equivale a typename in questo contesto. È possibile esprimere l'esempio precedente come:

template <class T, class U, class V> class Foo{};

È possibile usare l'operatore con i puntini di sospensione (...) per definire un modello che accetta un numero arbitrario di zero o più parametri di tipo:

template<typename... Arguments> class vtclass;

vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;

Qualsiasi tipo predefinito o definito dall'utente può essere usato come argomento di tipo. Ad esempio, è possibile usare std::vector nella libreria standard per archiviare variabili di tipo int, , doublestd::string, MyClass, constMyClass*, MyClass&e così via. La restrizione principale quando si usano i modelli è che un argomento di tipo deve supportare tutte le operazioni applicate ai parametri di tipo. Ad esempio, se si chiama minimum usando MyClass come in questo esempio:

class MyClass
{
public:
    int num;
    std::wstring description;
};

int main()
{
    MyClass mc1 {1, L"hello"};
    MyClass mc2 {2, L"goodbye"};
    auto result = minimum(mc1, mc2); // Error! C2678
}

Verrà generato un errore del compilatore perché MyClass non fornisce un overload per l'operatore < .

Non esiste alcun requisito intrinseco che gli argomenti di tipo per qualsiasi modello specifico appartengano alla stessa gerarchia di oggetti, anche se è possibile definire un modello che applica tale restrizione. È possibile combinare tecniche orientate agli oggetti con i modelli; ad esempio, è possibile archiviare un derivato* in base a vettori<*>. Si noti che gli argomenti devono essere puntatori

vector<MyClass*> vec;
   MyDerived d(3, L"back again", time(0));
   vec.push_back(&d);

   // or more realistically:
   vector<shared_ptr<MyClass>> vec2;
   vec2.push_back(make_shared<MyDerived>());

I requisiti di base che std::vector e altri contenitori di librerie standard impongono agli elementi di T è che T possono essere copiati e costruttibili dalla copia.

Parametri non di tipo

A differenza dei tipi generici in altri linguaggi, ad esempio C# e Java, i modelli C++ supportano parametri non di tipo, detti anche parametri valore. Ad esempio, è possibile fornire un valore integrale costante per specificare la lunghezza di una matrice, come in questo esempio simile alla classe std::array nella libreria standard:

template<typename T, size_t L>
class MyArray
{
    T arr[L];
public:
    MyArray() { ... }
};

Si noti la sintassi nella dichiarazione del modello. Il size_t valore viene passato come argomento modello in fase di compilazione e deve essere const o un'espressione constexpr . È possibile usarlo come segue:

MyArray<MyClass*, 10> arr;

Altri tipi di valori, inclusi puntatori e riferimenti, possono essere passati come parametri non di tipo. Ad esempio, è possibile passare un puntatore a una funzione o a un oggetto funzione per personalizzare alcune operazioni all'interno del codice del modello.

Deduzione del tipo per i parametri di modello non di tipo

In Visual Studio 2017 e versioni successive e in /std:c++17 modalità o successiva il compilatore deduce il tipo di un argomento modello non di tipo dichiarato con auto:

template <auto x> constexpr auto constant = x;

auto v1 = constant<5>;      // v1 == 5, decltype(v1) is int
auto v2 = constant<true>;   // v2 == true, decltype(v2) is bool
auto v3 = constant<'a'>;    // v3 == 'a', decltype(v3) is char

Modelli come parametri del modello

Un modello può essere un parametro di modello. In questo esempio MyClass2 ha due parametri di modello: un parametro typename T e un parametro di modello Arr:

template<typename T, template<typename U, int I> class Arr>
class MyClass2
{
    T t; //OK
    Arr<T, 10> a;
    U u; //Error. U not in scope
};

Poiché il parametro Arr stesso non ha corpo, i relativi nomi di parametro non sono necessari. In effetti, è un errore fare riferimento ai nomi dei parametri di classe o nometipo di Arr dall'interno del corpo di MyClass2. Per questo motivo, i nomi dei parametri di tipo di Arr possono essere omessi, come illustrato in questo esempio:

template<typename T, template<typename, int> class Arr>
class MyClass2
{
    T t; //OK
    Arr<T, 10> a;
};

Argomenti modello predefiniti

I modelli di classe e di funzione possono avere argomenti predefiniti. Quando un modello ha un argomento predefinito, è possibile lasciarlo non specificato quando viene usato. Ad esempio, il modello std::vector ha un argomento predefinito per l'allocatore:

template <class T, class Allocator = allocator<T>> class vector;

Nella maggior parte dei casi la classe std::allocator predefinita è accettabile, quindi si usa un vettore simile al seguente:

vector<int> myInts;

Tuttavia, se necessario, è possibile specificare un allocatore personalizzato come segue:

vector<int, MyAllocator> ints;

Per più argomenti di modello, tutti gli argomenti dopo il primo argomento predefinito devono avere argomenti predefiniti.

Quando si usa un modello i cui parametri sono tutti predefiniti, usare parentesi angolari vuote:

template<typename A = int, typename B = double>
class Bar
{
    //...
};
...
int main()
{
    Bar<> bar; // use all default type arguments
}

Specializzazione modello

In alcuni casi, non è possibile o auspicabile che un modello definisci esattamente lo stesso codice per qualsiasi tipo. Ad esempio, è possibile definire un percorso di codice da eseguire solo se l'argomento di tipo è un puntatore o std::wstring o un tipo derivato da una determinata classe di base. In questi casi è possibile definire una specializzazione del modello per quel particolare tipo. Quando un utente crea un'istanza del modello con tale tipo, il compilatore usa la specializzazione per generare la classe e per tutti gli altri tipi, il compilatore sceglie il modello più generale. Le specializzazioni in cui tutti i parametri sono specializzati sono specializzazioni complete. Se solo alcuni dei parametri sono specializzati, viene chiamato specializzazione parziale.

template <typename K, typename V>
class MyMap{/*...*/};

// partial specialization for string keys
template<typename V>
class MyMap<string, V> {/*...*/};
...
MyMap<int, MyClass> classes; // uses original template
MyMap<string, MyClass> classes2; // uses the partial specialization

Un modello può avere un numero qualsiasi di specializzazioni, purché ogni parametro di tipo specializzato sia univoco. Solo i modelli di classe possono essere parzialmente specializzati. Tutte le specializzazioni complete e parziali di un modello devono essere dichiarate nello stesso spazio dei nomi del modello originale.

Per altre informazioni, vedere Specializzazione del modello.