Exploração aprofundada de modelos C++: do básico aos aplicativos avançados

Índice

1. Programação genérica

1.1 Por que precisamos de programação genérica?

2. Modelo

2.1 Conceito

2.2 Modelos de função

2.2.1 Conceito

2.2.2 Gramática

2.2.3 Exemplos

 2.2.4 Instanciação do modelo

instanciação implícita

exibir instanciação

2.2.5 Princípio de correspondência dos parâmetros do modelo

2.3 Modelos de Aula

2.3.1 Conceito

2.3.2 Sintaxe

2.3.3 Exemplos

2.3.4 Precauções

2.3.5 Notas sobre Interpretação

1. Definição de função de membro:

2. Derivação do parâmetro do modelo:

3. Especialização em modelos:

3. Escrita do arquivo modelo (especial)

1. Programação genérica

Quando falamos de programação genérica, geralmente nos referimos a um método de escrever código genérico em uma linguagem de programação para funcionar eficientemente em diferentes tipos de dados. Isso torna o código mais flexível, genérico e reutilizável. Em C++, a programação genérica é implementada principalmente por meio de templates, permitindo criar código genérico que pode ser aplicado a vários tipos de dados.

1.1 Por que precisamos de programação genérica?

Ao escrever código, muitas vezes precisamos lidar com vários tipos de dados. Escrever código especializado para cada tipo de dados a cada vez resultará em redundância de código e reduzirá a eficiência. O objetivo da programação genérica é resolver esse problema criando um código genérico que pode ser aplicado a diferentes tipos de dados sem a necessidade de reescrevê-lo.

por exemplo:

Queremos implementar uma função geral para troca de dados. Em linguagem C, só podemos implementar funções com nomes diferentes, o que é muito problemático. Em C++, podemos implementar sobrecarga de função, mas também há desvantagens: as funções sobrecarregadas são apenas de tipos diferentes , a taxa de reutilização do código é relativamente baixa, desde que um novo tipo apareça, você precisa adicionar manualmente a implementação da função correspondente e a capacidade de manutenção do código também diminuirá!

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;
}

//其他类型
.........

Então, há alguma outra maneira? Eu só preciso fornecer o tipo que preciso e o compilador implementará automaticamente a versão correspondente da função!

Os modelos C++ são uma ferramenta poderosa para resolver esse problema.

2. Modelo

2.1 Conceito

Os modelos C++ são um recurso importante para a implementação de programação genérica, permitindo que você escreva um código genérico que pode ser aplicado a vários tipos de dados. Os modelos são amplamente usados ​​em C++, especialmente na biblioteca padrão, para criar contêineres, algoritmos e estruturas de dados comuns.

Existem dois tipos principais de modelos em C++: modelos de função e modelos de classe .

2.2 Modelos de função

2.2.1 Conceito

Os modelos de função são um mecanismo em C++ para criar funções genéricas que permitem escrever uma função genérica que pode ser usada com vários tipos de dados sem precisar escrever funções separadas para cada tipo de dados. O modelo de função é uma das ferramentas importantes para realizar a programação genérica em C++.

2.2.2 Gramática

A sintaxe de um modelo de função é simples, consiste em um cabeçalho de modelo e um corpo de função :

 Entre eles, template <typename T>é declarado um template, que é um espaço reservadoT para os parâmetros de tipo . Usado em parâmetros de função e tipos de retorno , o compilador determinará automaticamente o tipo de dados real de acordo com o tipo de parâmetro. (typename é usado para definir as palavras-chave dos parâmetros do modelo, e class também pode ser usado ( lembre-se: struct não pode ser usado no lugar de class)).T

2.2.3 Exemplos

É possível criar funções genéricas que podem ser utilizadas com diferentes tipos de dados, como por exemplo a função maximum:

 2.2.4 Instanciação do modelo

A instanciação do modelo de função refere-se ao processo no qual o compilador gera um tipo específico de código de implementação de função de acordo com o tipo de parâmetro real passado ao usar o modelo de função. Esse processo ocorre durante a fase de compilação para garantir que o código correto seja gerado para parâmetros de um tipo específico.

A instanciação do parâmetro do modelo é dividida em: instanciação implícita e instanciação explícita

instanciação implícita

Isso significa que quando um modelo de função é chamado no código, o compilador gera implicitamente uma implementação de função de um tipo de dados específico de acordo com o tipo de parâmetro passado. Essa é uma maneira comum de instanciar modelos de função.

template <typename T>
T Max(T a, T b) {
    return a > b ? a : b;
}

int main() {
    int result = Max(5, 10); // 隐式实例化为 int Max(int a, int b)
    return 0;
}

Supondo que chamemos Max(5, 10), o compilador executará as seguintes etapas para instanciar o modelo de função:

  1. O compilador vê a chamada Max(5, 10)e precisa instanciar o modelo de função Max.
  2. O compilador analisa os parâmetros 5e 10, para determinar quais são seus tipos int.
  3. O compilador Tsubstitui o parâmetro template por int, gerando uma implementação específica como esta:

 Então, como eu chamo assim?

int a = 2;
double b = 3.0;

Max(a, b);

natureza do problema

Esta instrução não pode ser compilada . Quando o compilador encontra vários parâmetros reais de tipos diferentes, ele precisa determinar um tipo de parâmetro de modelo adequado. No entanto, em alguns casos pode ocorrer que o tipo do parâmetro do modelo não possa ser determinado de forma inequívoca porque há apenas um parâmetro do modelo na lista de parâmetros do modelo, mas o argumento real pode ser de um tipo diferente.

Inferência de tipo e conversão de tipo

Durante a instanciação do modelo, o compilador geralmente não executa conversões de tipo implícitas, pois isso pode levar a resultados ambíguos. Por exemplo, no seu caso, o compilador não sabe se deve Tinferir o parâmetro do modelo como intou doublee, portanto, não pode fazer uma instanciação correta.

Observação: em modelos, o compilador geralmente não executa operações de conversão de tipo

Existem duas maneiras comuns de lidar com isso:

1. Conversão forçada pelo usuário

2. Instanciação explícita

conversão forçada

Max(a, (int)b);

//或者

Max((double)a, b);
exibir instanciação

Especificar o tipo real do parâmetro do modelo em <> após o nome da função pode informar explicitamente ao compilador o tipo de instanciação para evitar o problema de inferência do parâmetro do modelo

Max<int>(a, b);

//或者

Max<double>(a, b);

Se os tipos não corresponderem, o compilador tentará realizar uma conversão implícita de tipo e, se a conversão falhar, o compilador relatará um erro .

2.2.5 Princípio de correspondência dos parâmetros do modelo

Quando houver funções sem modelo e modelos de função com o mesmo nome, o compilador selecionará a função apropriada de acordo com as regras correspondentes ao chamar a função. Detalhes a seguir:

  1. Prefira funções não padrão:

    Se houver uma função sem modelo que corresponda exatamente ao tipo do parâmetro real, o compilador preferirá chamar essa função sem modelo porque ela é mais especializada em correspondência de tipo.
  2. Correspondência de parâmetros do modelo:

    Se não houver nenhuma função não modelo que corresponda exatamente ao tipo de parâmetro real, o compilador tentará executar a correspondência de parâmetros de modelo para encontrar o melhor modelo de função correspondente.
  3. Uma correspondência melhor:

    Se a função de modelo puder gerar uma instância de correspondência melhor, o compilador escolherá chamar essa função de modelo. Por exemplo, se os parâmetros do modelo puderem corresponder melhor aos parâmetros reais por meio da conversão implícita de tipo, o modelo será escolhido.

Vamos dar uma olhada em um exemplo

#include <iostream>

// 通用输出函数模板
template<class T>
void Print(T value) {
    std::cout << value << std::endl;
}

// 重载的输出函数,专门处理 const char* 类型
void Print(const char* value) {
    std::cout << "String: " << value << std::endl;
}

int main() {
    Print(42);          // 调用通用函数模板 Print<T>
    Print("Hello");     // 调用重载函数 Print(const char*)
    Print(3.14);        // 调用通用函数模板 Print<T>
    Print<const char*>("Hello");    //显示调用通用函数模板 Print<T>

    return 0;
}
  1. template<class T> void Print(T value)é um modelo de função de saída genérico para saída de diferentes tipos de dados.

  2. void Print(const char* value)é uma const char*sobrecarga da função de saída que lida especificamente com o tipo.

  3. Em mainuma função, a chamada Print(42)corresponderá ao modelo de função genérica porque os tipos correspondem.

  4. A chamada Print("Hello")corresponderá à função sobrecarregada porque const char*é mais especializada.

  5. A chamada Print(3.14)ainda corresponderá ao modelo de função genérico.

  6. Mostrando o modelo de chamada, Print<const char*>("Hello") obviamente chamará o modelo de função.

outro exemplo

#include <iostream>

// 专门处理 int 的加法函数
int Add(int left, int right) {
    return left + right;
}

// 通用加法函数模板
template<class T1, class T2>
T1 Add(T1 left, T2 right) {
    return left + right;
}

void Test() {
    Add(1, 2);       // 调用非函数模板,与 int Add(int left, int right) 匹配
    Add(1, 2.0);     // 调用函数模板,生成更匹配的版本
}
  1. int Add(int left, int right)é uma intfunção de adição que lida especificamente com o tipo.

  2. template<class T1, class T2> T1 Add(T1 left, T2 right)é um modelo genérico de função de adição que pode ser aplicado a diferentes tipos de dados.

  3. Ao chamar Add(1, 2), o compilador opta por chamar o modelo não funcional porque ele corresponde exatamente aos tipos de argumento reais.

  4. Ao chamar Add(1, 2.0), o compilador opta por chamar o modelo de função. Embora os modelos não funcionais também possam corresponder, os modelos de função podem gerar uma versão mais correspondente e o compilador gerará uma Addfunção mais correspondente com base nos parâmetros reais.

2.3 Modelos de Aula

2.3.1 Conceito

Quando se trata de modelos de classe, você está criando um modelo que pode gerar diferentes classes com base em diferentes tipos de dados. Os modelos de classe permitem que você escreva definições de classe genéricas que se aplicam a vários tipos de dados sem precisar escrever código separado para cada tipo.

2.3.2 Sintaxe

A sintaxe de um modelo de classe é semelhante à de um modelo de função, mas aplicada à definição de uma classe. Aqui está um exemplo de um modelo de classe simples:

template <class T>
class MyContainer {
private:
    T value;

public:
    MyContainer(T val) : value(val) {}

    T GetValue() {
        return value;
    }
};

2.3.3 Exemplos

int main() {
    MyContainer<int> intContainer(42);
    MyContainer<double> doubleContainer(3.14);

    std::cout << intContainer.GetValue() << std::endl;    // 输出: 42
    std::cout << doubleContainer.GetValue() << std::endl; // 输出: 3.14

    return 0;
}

2.3.4 Precauções

  1. Definição de função de membro: funções de membro de modelos de classe geralmente também precisam ser definidas dentro da classe de modelo, caso contrário, palavras-chave precisam ser usadas fora da definição templatepara declaração e implementação de modelo.

  2. Dedução de parâmetro de modelo: ao instanciar um modelo de classe, o compilador pode deduzir automaticamente o tipo do parâmetro de modelo ou usar um método explicitamente especificado para instanciação.

  3. Especialização de modelo: os modelos de classe também podem ser especializados para fornecer implementações personalizadas para tipos específicos, semelhante à especialização de modelo de modelos de função.

  4. Geração de código na instanciação: quando um modelo de classe é instanciado, o compilador gera uma definição de classe correspondente com base nos parâmetros de tipo reais, criando assim uma classe de um tipo específico.

2.3.5 Notas sobre Interpretação

1. Definição de função de membro:

Em um modelo de classe, as funções de membro podem ser definidas dentro da classe de modelo e também podem ser templatedeclaradas e implementadas fora da classe usando palavras-chave.

template <class T>
class MyContainer {
private:
    T value;

public:
    MyContainer(T val) : value(val) {}

    T GetValue() {
        return value;
    }
};

// 在类外部定义成员函数模板
template <class T>
T MyContainer<T>::GetValue() {
    return value * 2;
}

2. Derivação do parâmetro do modelo:

Quando o compilador instancia um modelo de classe, ele pode deduzir automaticamente o tipo do parâmetro do modelo ou pode usar um método explicitamente especificado para instanciação.

int main() {
    MyContainer intContainer(42);
    MyContainer<double> doubleContainer(3.14);

    std::cout << intContainer.GetValue() << std::endl;    // 输出: 42
    std::cout << doubleContainer.GetValue() << std::endl; // 输出: 3.14

    return 0;
}

3. Especialização em modelos:

Os modelos de classe também podem ser especializados para fornecer implementações personalizadas para tipos específicos, semelhante à especialização de modelo de modelos de função.

// 类模板定义
template <class T>
class MyContainer {
private:
    T value;

public:
    MyContainer(T val) : value(val) {}

    T GetValue() {
        return value;
    }
};

// 类模板的特化版本
template <>
class MyContainer<int> {
private:
    int value;

public:
    MyContainer(int val) : value(val) {}

    int GetValue() {
        return value * 2; // 自定义的实现
    }
};

3. Escrita do arquivo modelo (especial)

Diferente da gravação de arquivo de código normal, a gravação de arquivo de modelo envolve algumas precauções e métodos especiais para garantir a instanciação correta e a vinculação de modelos. Darei um exemplo simples de como dividir modelos em cabeçalho e arquivos de origem.

Exemplo:

Suponha que temos um modelo de classe MyTemplate, que contém funções de membro, e queremos separá-lo em cabeçalho e arquivos de origem.

MyTemplate.h (arquivo de cabeçalho):

#ifndef MYTEMPLATE_H
#define MYTEMPLATE_H

template <typename T>
class MyTemplate {
public:
    MyTemplate(T value);
    void PrintValue();
    
private:
    T data;
};

#endif

MyTemplate.cpp (arquivo de origem):

#include <iostream>
#include "MyTemplate.h"

template <typename T>
MyTemplate<T>::MyTemplate(T value) : data(value) {}

template <typename T>
void MyTemplate<T>::PrintValue() {
    std::cout << "Value: " << data << std::endl;
}

// 显式实例化模板
template class MyTemplate<int>;
template class MyTemplate<double>;

main.cpp (arquivo principal):

#include "MyTemplate.h"

int main() {
    MyTemplate<int> intObj(42);
    MyTemplate<double> doubleObj(3.14);
    
    intObj.PrintValue();
    doubleObj.PrintValue();
    
    return 0;
}

No arquivo de origem MyTemplate.cpp, fornecemos a implementação do modelo de classe , incluindo a definição do construtor e das funções de membro. Também usamos instanciação explícita ( template class MyTemplate<int>;e template class MyTemplate<double>;) no arquivo de origem para garantir que um tipo específico de instância de modelo seja gerado durante a compilação.

Por fim, no arquivo principal main.cpp, basta incluir o arquivo de cabeçalho MyTemplate.he usar o modelo de classe. Ao compilar, o compilador irá extrair a parte de implementação MyTemplate.cppdo e instanciá-lo.

Embora essa maneira de escrever modelos em arquivos separados exija algumas etapas extras, ela torna o código mais organizado e fácil de manter.

Explicação adicional

MyTemplate.cppA instanciação explícita é usada em arquivos de origem para garantir que o compilador gere código de instanciação de modelo para um tipo específico durante a compilação. Embora a definição de um modelo de função geralmente seja colocada em um arquivo de cabeçalho, devido ao modelo de compilação separado do C++, a implementação do modelo deve estar na mesma unidade de compilação para que o compilador possa instanciá-lo quando necessário.

Na implementação de modelos de classe, uma vez que os parâmetros do modelo podem ser de vários tipos diferentes, o compilador não gerará automaticamente o código de instanciação para cada tipo, a menos que o modelo seja usado explicitamente quando necessário. É por isso que a instanciação explícita é usada: você diz ao compilador para gerar o código de instanciação para um tipo específico, para garantir que ele encontre a implementação de modelo correta no momento do link.

O mesmo princípio se aplica à escrita de funções em arquivos separados! ! !

Este é o fim do básico dos modelos de função! O autor continuará atualizando o tutorial de modelo avançado! !

Acho que você gosta

Origin blog.csdn.net/weixin_57082854/article/details/132178260
Recomendado
Clasificación