Introdução aos modelos C++
1. Programação genérica
Suponha que queiramos implementar uma função de troca e oferecer suporte a diferentes tipos de implementações de parâmetros. Podemos usar typedef para renomear o tipo, como o seguinte código:
// 将 int 起别名为 DataType
typedef int DataType;
void Swap(DataType& x, DataType& y)
{
DataType tmp = x;
x = y;
y = tmp;
}
int main()
{
DataType x = 0, y = 6;
Swap(x, y);
return 0;
}
Desta forma, toda vez que precisarmos mudar o tipo, só precisamos mudar o int para outro tipo;
O acima é um método e há outro método que pode ser implementado usando sobrecarga de função, por exemplo:
void Swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;
}
void Swap(double& left, double& right)
{
double temp = left;
left = right;
right = temp;
}
void Swap(char& left, char& right)
{
char temp = left;
left = right;
right = temp;
}
Embora os dois métodos acima possam implementar funções de troca universais, eles têm as seguintes desvantagens:
- As funções sobrecarregadas possuem apenas tipos diferentes e a taxa de reutilização de código é relativamente baixa.Enquanto um novo tipo aparecer, o usuário precisará adicionar a função correspondente ou modificar o tipo.
- A capacidade de manutenção do código é relativamente baixa e um erro pode causar erros em todas as sobrecargas.
Você pode informar um modelo ao compilador e permitir que o compilador use o modelo para gerar código de acordo com diferentes tipos? A resposta é sim, aqui você precisa introduzir a programação genérica.Programação genérica : escrever código universal independente do tipo é um meio de reutilização de código. Os modelos são a base da programação genérica.
Os modelos são divididos em modelos de função e modelos de classe .
2. Modelo de função
1. O conceito de modelo de função
Um modelo de função representa uma família de funções que são independentes de tipo e são parametrizadas quando usadas para produzir uma versão específica do tipo da função com base nos tipos de parâmetros reais.
2. Formato do modelo de função
Antes de definir o modelo de função, precisamos introduzir uma palavra-chave: template , que é a palavra-chave para definir o modelo;
Formato de uso: template<typename T1, typename T2,......,typename Tn>
, use colchetes angulares para colocar os parâmetros do modelo após a palavra-chave do modelo. O número de parâmetros do modelo pode ser arbitrário, mas você precisa usar a palavra-chave typename para definir os parâmetros do modelo, ou você pode usar classe (lembre-se: você não pode use struct em vez de class ).
Por exemplo, o modelo de função da função de troca:
template<typename T>
void Swap(T& t1, T& t2)
{
T tmp = t1;
t1 = t2;
t2 = tmp;
}
3. Princípio do modelo de função
Um modelo de função é um modelo, que não é uma função em si. É um molde que o compilador usa para gerar um tipo específico de função. Então, na verdade, o modelo é entregar as coisas repetitivas que devemos fazer ao compilador.
Por exemplo, a figura a seguir ilustra muito bem esse processo:
Na fase de compilação do compilador, para usar funções de modelo, o compilador precisa deduzir e gerar tipos correspondentes de funções para chamar com base nos tipos de parâmetros reais passados.
Por exemplo: ao usar um modelo de função com um tipo duplo , o compilador determina T como um tipo duplo através da dedução do parâmetro real type e, em seguida, gera um código que trata especificamente do tipo duplo. O mesmo se aplica aos tipos de caracteres, que isto é, o compilador usa instâncias de modelo para gerar a função Swap correspondente.
4. Instanciação do modelo de função
Quando um modelo de função é usado com parâmetros de tipos diferentes, isso é chamado de instanciação do modelo de função. A instanciação de parâmetros de modelo é dividida em: instanciação implícita e instanciação explícita .
Instanciação implícita: deixe o compilador deduzir o tipo real do parâmetro do modelo com base nos parâmetros reais.Por exemplo, o seguinte modelo de função Add implementa a adição de dois números:
template<class T>
T Add(T a, T b)
{
return a + b;
}
int main()
{
int a = 10, b = 20;
double c = 1.11, d = 2.22;
cout << "sum = " << Add(a, b) << endl;
cout << "sum = " << Add(c, d) << endl;
return 0;
}
Não há problema com a instanciação das duas chamadas acima. O compilador executa a instanciação implícita e os resultados da execução são os seguintes:
Mas ele será compilado se for chamado assim: Add(a, d)
, a resposta é não, T é deduzido para int por meio do parâmetro real a e T é deduzido para double tipo por meio do parâmetro real d , mas há apenas um T na lista de parâmetros do modelo , e o compilador não pode determinar isso. No final, T determinado como sendo do tipo int ou double e um erro será relatado.
Portanto, existem duas soluções neste momento:
- Os próprios usuários forçam a conversão
- Use instanciação explícita
Se quiser forçar a conversão sozinho, você pode usar o seguinte método:
int main()
{
int a = 10, b = 20;
double c = 1.11, d = 2.22;
cout << "sum = " << Add((double)a, d) << endl;
cout << "sum = " << Add(a, (int)d) << endl;
return 0;
}
Podemos forçar a a double ou d a int ao chamar a função Add .
Instanciação explícita: Especifique o tipo real do parâmetro do modelo em <> após o nome da função.
Por exemplo, no problema acima, usamos instanciação explícita para resolver o problema. O código é o seguinte:
int main()
{
int a = 10, b = 20;
double c = 1.11, d = 2.22;
cout << "sum = " << Add(a, b) << endl;
cout << "sum = " << Add(c, d) << endl;
cout << "sum = " << Add<double>(a, d) << endl;
cout << "sum = " << Add<int>(a, d) << endl;
return 0;
}
Usamos colchetes angulares após o nome da função para especificar o tipo do parâmetro do modelo. Esta é uma instanciação explícita.
Nota: Se os tipos não corresponderem, o compilador tentará realizar a conversão implícita de tipo. Se a conversão não puder ser bem-sucedida, o compilador reportará um erro.
5. Princípios de correspondência de parâmetros de modelo
Para funções não-modelo e modelos de função com o mesmo nome , se outras condições forem iguais, a função não-modelo será chamada primeiro durante a chamada e uma instância não será gerada a partir do modelo. O modelo será escolhido se puder produzir uma função com melhor correspondência.
Por exemplo, os dois trechos de código a seguir:
// 专门处理int的加法函数
int Add(int a, int b)
{
cout << "int Add(int a, int b)" << endl;
return a + b;
}
// 通用加法函数
template<class T>
T Add(T a, T b)
{
cout << "T Add(T a, T b)" << endl;
return a + b;
}
int main()
{
// 与非函数模板类型完全匹配,不需要函数模板实例化
Add(1, 2);
// 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的 Add 函数
Add<int>(1, 2);
return 0;
}
3. Modelo de aula
Suponha que precisemos implementar uma pilha universal. Podemos usar a palavra-chave typedef para alias do tipo. Cada vez que precisarmos alterar o tipo, precisamos apenas alterar o typedef . Por exemplo, a seguinte classe Stack :
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = new DataType[capacity];
_capacity = capacity;
_size = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
private:
// 内置类型
DataType* _array;
int _capacity;
int _size;
};
Embora os diferentes tipos da classe Stack acima precisem apenas alterar o tipo typedef , se eu precisar de duas pilhas ao mesmo tempo, o parâmetro de uma pilha é int e o parâmetro da outra pilha é double , o método acima não funcionará Bem. Satisfeito, então apresentamos modelos de classe.
O uso de modelos de classe é o seguinte, tomando a classe Stack como exemplo:
template<class T>
class Stack
{
public:
Stack(size_t capacity = 4)
{
_array = new T[capacity];
_capacity = capacity;
_size = 0;
}
~Stack()
{
delete[] _array;
_array = nullptr;
_size = _capacity = 0;
}
private:
T* _array;
int _capacity;
int _size;
};
O objeto instanciado é o seguinte:
int main()
{
Stack<int> st1;
Stack<double> st2;
return 0;
}
Observe que Stack é o nome da classe Stack<int>
e Stack<double>
é o tipo; o escopo do modelo é a classe Stack .
Desta forma, implementamos duas pilhas ao mesmo tempo. Uma pilha armazena parâmetros de int e a outra armazena double .