Modelos (C++)

Os modelos são a base para programação genérica em C++. Como uma linguagem fortemente tipada, o C++ exige que todas as variáveis tenham um tipo específico, declarado explicitamente pelo programador ou deduzido pelo compilador. No entanto, muitas estruturas de dados e algoritmos têm a mesma aparência, independentemente do tipo em que estão operando. Os modelos permitem definir as operações de uma classe ou função e possibilitam que o usuário especifique em quais tipos concretos essas operações devem funcionar.

Definir e usar modelos

Um modelo é um constructo que gera um tipo ou função comum em tempo de compilação com base nos argumentos fornecidos pelo usuário para os parâmetros de modelo. Por exemplo, você pode definir um modelo de função como este:

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

O código acima descreve um modelo para uma função genérica com um único parâmetro de tipo T, cujos parâmetros de valor de retorno e chamada (lhs e rhs) são todos desse tipo. Você pode nomear um parâmetro de tipo como quiser, mas, por convenção, letras maiúsculas e minúsculas são mais usadas. T é um parâmetro de modelo; a palavra-chave typename diz que esse parâmetro é um espaço reservado para um tipo. Quando a função é chamada, o compilador substituirá todas as instâncias de T pelo argumento de tipo concreto especificado pelo usuário ou deduzido pelo compilador. O processo no qual o compilador gera uma classe ou função de um modelo é chamado de instanciação de modelo; minimum<int> é uma instanciação do modelo minimum<T>.

Em outro lugar, um usuário pode declarar uma instância do modelo que é especializada para int. Suponha que get_a() e get_b() são funções que retornam um int:

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

No entanto, como esse é um modelo de função e o compilador pode deduzir o tipo de T dos argumentos a e b, você pode chamá-lo como uma função comum:

int i = minimum(a, b);

Quando o compilador encontra essa última instrução, ele gera uma nova função na qual cada ocorrência de T no modelo é substituída por int:

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

As regras de como o compilador executa a dedução de tipo em modelos de função são baseadas nas regras para funções comuns. Para obter mais informações, confira Resolução de sobrecarga de chamadas de modelo de função.

Parâmetros de tipo

No modelo minimum acima, observe que o parâmetro de tipo T não é qualificado de forma alguma até que seja usado nos parâmetros de chamada de função, em que são adicionados os qualificadores const e de referência.

Não há limite prático para o número de parâmetros de tipo. Separe vários parâmetros por vírgulas:

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

A palavra-chave class é equivalente a typename neste contexto. Você pode expressar o exemplo anterior como:

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

Você pode usar o operador de reticências (...) para definir um modelo que usa um número arbitrário de zero ou mais parâmetros de tipo:

template<typename... Arguments> class vtclass;

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

Qualquer tipo interno ou definido pelo usuário pode ser usado como um argumento de tipo. Por exemplo, você pode usar std::vector na Biblioteca Padrão para armazenar variáveis de tipo int, double, std::string, MyClass, constMyClass*, MyClass& e assim por diante. A principal restrição ao usar modelos é que um argumento de tipo deve dar suporte a quaisquer operações aplicadas aos parâmetros de tipo. Por exemplo, se chamarmos minimum usando MyClass como neste exemplo:

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
}

Um erro do compilador será gerado porque MyClass não fornece uma sobrecarga para o operador <.

Não há nenhum requisito inerente de que os argumentos de tipo para qualquer modelo específico pertençam à mesma hierarquia de objetos, embora você possa definir um modelo que imponha essa restrição. Você pode combinar técnicas orientadas a objeto com modelos; por exemplo, você pode armazenar um Derivado* em um vetor<Base*>. Observe que os argumentos devem ser ponteiros

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>());

Os requisitos básicos que std::vector e outros contêineres de biblioteca padrão impõem aos elementos T é que T sejam atribuíveis e construtíveis por cópia.

Parâmetros não tipo

Ao contrário de tipos genéricos em outras linguagens, como C# e Java, os modelos C++ dão suporte a parâmetros não tipo, também chamados de parâmetros de valor. Por exemplo, você pode fornecer um valor integral constante para especificar o comprimento de uma matriz, como neste exemplo semelhante à classe std::array na Biblioteca Padrão:

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

Observe a sintaxe na declaração do modelo. O valor size_t é passado como um argumento de modelo em tempo de compilação e deve ser uma expressão const ou constexpr. Use-o da seguinte maneira:

MyArray<MyClass*, 10> arr;

Outros tipos de valores, incluindo ponteiros e referências, podem ser passados como parâmetros não tipo. Por exemplo, você pode passar um ponteiro para uma função ou objeto de função para personalizar uma operação dentro do código do modelo.

Dedução de tipo para parâmetros de modelo não tipo

No Visual Studio 2017 e posteriores, e no modo /std:c++17 ou posteriores, o compilador deduz o tipo de um argumento de modelo não tipo que foi declarado com 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

Modelos como parâmetros de modelo

Um modelo pode ser um parâmetro de modelo. Neste exemplo, MyClass2 tem dois parâmetros de modelo: um parâmetro de nome de tipo T e um parâmetro de modelo 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
};

Como o parâmetro Arr em si não tem corpo, seus nomes de parâmetro não são necessários. Na verdade, é um erro fazer referência aos nomes de parâmetro de classe ou nome de tipo do Arr dentro do corpo de MyClass2. Por esse motivo, os nomes de parâmetro de tipo do Arr podem ser omitidos, conforme mostrado neste exemplo:

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

Argumentos do modelo padrão

Modelos de classe e função podem ter argumentos padrão. Quando um modelo tem um argumento padrão, você pode deixá-lo não especificado ao usá-lo. Por exemplo, o modelo std::vector tem um argumento padrão para o alocador:

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

Na maioria dos casos, a classe padrão std::allocator é aceitável, portanto, você usa um vetor como este:

vector<int> myInts;

Mas, se necessário, você pode especificar um alocador personalizado como este:

vector<int, MyAllocator> ints;

Para mais argumentos de modelo, todos os argumentos após o primeiro argumento padrão devem ter argumentos padrão.

Ao usar um modelo cujos parâmetros são todos padrão, use colchetes angulares vazios:

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

Especialização de modelo

Em alguns casos, não é possível ou desejável que um modelo defina exatamente o mesmo código para qualquer tipo. Por exemplo, talvez você deseje definir um caminho do código a ser executado somente se o argumento de tipo for um ponteiro, ou um std::wstring, ou um tipo derivado de uma classe base específica. Nesses casos, você pode definir uma especialização do modelo para esse tipo específico. Quando um usuário cria uma instância do modelo com esse tipo, o compilador usa a especialização para gerar a classe e, para todos os outros tipos, o compilador escolhe o modelo mais geral. Especializações nas quais todos os parâmetros são especializados são especializações completas. Se apenas alguns dos parâmetros forem especializados, ele será chamado de especialização parcial.

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

Um modelo pode ter qualquer número de especializações, desde que cada parâmetro de tipo especializado seja exclusivo. Somente modelos de classe podem ser parcialmente especializados. Todas as especializações completas e parciais de um modelo devem ser declaradas no mesmo namespace do modelo original.

Para obter mais informações, confira Especificações de Modelo.