Sari la conținut

Class Templates & CTAD

Build generic data structures with class templates. Understand member definitions, default arguments, and how C++17's CTAD lets you omit template arguments.

Class Templates

A class template defines a family of classes parameterized by one or more types (or non-type values). The standard library is built on class templates:std::vector,std::map,std::optional,std::shared_ptr. When you writestd::vector, the compiler instantiates the entire class withT = int, generating a concrete type.

Class templates follow the same rules as function templates: they must be defined in headers, and each unique set of template arguments produces a distinct type.StackandStackare completely unrelated types — they share no inheritance relationship.

O stivă generică

Iată o stivă generică simplă. Observați cum funcțiile membru definite în afara corpului clasei necesită repetarea listei de parametri șablon.

stack.hpp
#include <vector>
#include <stdexcept>
#include <iostream>

template <typename T>
class Stack {
private:
    std::vector<T> elements_;

public:
    void push(const T& value);
    void pop();
    const T& top() const;
    bool empty() const { return elements_.empty(); }
    std::size_t size() const { return elements_.size(); }
};

// Member definitions outside the class require the template header
template <typename T>
void Stack<T>::push(const T& value) {
    elements_.push_back(value);
}

template <typename T>
void Stack<T>::pop() {
    if (elements_.empty()) {
        throw std::out_of_range("Stack<>::pop(): empty stack");
    }
    elements_.pop_back();
}

template <typename T>
const T& Stack<T>::top() const {
    if (elements_.empty()) {
        throw std::out_of_range("Stack<>::top(): empty stack");
    }
    return elements_.back();
}

int main() {
    Stack<int> intStack;
    intStack.push(42);
    intStack.push(17);
    std::cout << intStack.top() << '\n'; // 17
    intStack.pop();
    std::cout << intStack.top() << '\n'; // 42
}

Parametri multipli și valori implicite

Șabloanele de clasă pot avea parametri de șablon multipli și argumente de șablon implicit, la fel cum parametrii de funcție pot avea valori implicite. Argumentele implicite sunt rezolvate de la stânga la dreapta.

stack_defaults.cpp
#include <vector>
#include <deque>
#include <iostream>

// Container defaults to std::deque<T> — matches std::stack's design
template <typename T, typename Container = std::deque<T>>
class Stack {
private:
    Container elements_;

public:
    void push(const T& value) { elements_.push_back(value); }
    void pop()                { elements_.pop_back(); }
    const T& top() const      { return elements_.back(); }
    bool empty() const        { return elements_.empty(); }
};

int main() {
    Stack<int> s1;                         // uses deque<int>
    Stack<int, std::vector<int>> s2;       // uses vector<int>

    s1.push(10);
    s2.push(20);
    std::cout << s1.top() << '\n'; // 10
    std::cout << s2.top() << '\n'; // 20
}

Deducerea argumentelor șablon de clasă (CTAD)

Înainte de C++17, trebuia întotdeauna să specificați argumente șablon atunci când construiți instanțe șablon de clasă:std::pair p(1, 3.14);. CTAD (Class Template Argument Deduction), introdus în C++17, permite compilatorului să deducă argumente șablon de clasă din argumentele constructorului, la fel ca deducția argumentului șablonului funcției.

Cu CTAD:std::pair p(1, 3.14);— compilatorul deducestd::pair. Acest lucru funcționează pentrustd::vector,std::tuple,std::optional,std::array(C++20) și pentru propriile șabloane de clasă.

CTAD folosește ghiduri de deducție — fie implicit (generat din constructori) fie explicit (definit de utilizator). Compilatorul încearcă fiecare ghid și alege cea mai bună potrivire.

Ghiduri CTAD și deducere

Ghidurile de deducție definite de utilizator îi spun compilatorului cum să mapeze argumentele constructorului cu argumentele șablonului. Sunt utile în special atunci când tipul de argument al constructorului nu corespunde direct cu parametrul șablonului.

ctad_example.cpp
#include <iostream>
#include <cstddef>

template <typename T, std::size_t N>
struct Array {
    T data[N];
};

// Without a deduction guide, CTAD cannot deduce T and N.
// This explicit deduction guide maps brace-init with N elements
// of the same type T to Array<T, N>.
template <typename First, typename... Rest>
Array(First, Rest...) -> Array<First, 1 + sizeof...(Rest)>;

// Standard library example: std::vector CTAD from iterators
#include <vector>
#include <list>

int main() {
    // CTAD with our custom deduction guide
    Array arr{1, 2, 3, 4, 5};  // deduces Array<int, 5>
    std::cout << arr.data[2] << '\n'; // 3

    // Standard library CTAD examples (C++17)
    std::pair p(42, 3.14);       // pair<int, double>
    std::vector v{1, 2, 3};     // vector<int>
    std::tuple t(1, 2.0, 'a');  // tuple<int, double, char>

    // CTAD from iterator range
    std::list<int> lst{10, 20, 30};
    std::vector v2(lst.begin(), lst.end()); // vector<int>
}

Șabloane pentru membri

Funcțiile membre ale unui șablon de clasă pot fi ele însele șabloane, adăugând parametri de șablon suplimentați. Acesta se numește șablon de membru. Un caz obișnuit de utilizare este activarea atribuirii sau construcției dintr-o instanțiere conexă, dar diferită.

De exemplu, s-ar putea să doriți să atribuiți unStackde la unStack- dar deoarece acestea sunt tipuri neînrudite, atribuirea implicită de copiere nu va funcționa. Un operator de atribuire șablon de membru rezolvă acest lucru:

template <typename T>
class Stack {
public:
    template <typename U>
    Stack& operator=(const Stack<U>& other);
};

Inside this member template, U may differ from T. The body can copy elements from other as long as U is convertible to T. This pattern is used extensively in the standard library — std::shared_ptr converts to std::shared_ptr prin intermediul unui constructor de șablon membru.

Capcană

Când specializați un șablon de clasă, specializarea este o clasă complet nouă — nu moștenește niciun membru din șablonul principal. Trebuie să redefiniți fiecare funcție de membru și membru de date. Uitarea de a redefini un membru în specializare are ca rezultat o eroare de compilare atunci când acel membru este utilizat. De asemenea, specializarea parțială este disponibilă numai pentru șabloanele de clasă, nu și pentru șabloanele de funcție. Vom acoperi specializarea în detaliu în capitolul următor.

Idei cheie
  • Șabloanele de clasă definesc familii de clase — fiecare set unic de argumente produce un tip distinct
  • Funcțiile membre definite în afara clasei trebuie să repete antetultemplate și să foloseascăClassName::
  • Argumentele implicite ale șablonului funcționează de la stânga la dreapta, la fel ca argumentele implicite ale funcției
  • CTAD (C++17) permite compilatorului să deducă argumente șablon de clasă din argumentele constructorului
  • Deducția explicită ghidează modelele de argumente ale constructorului la argumentele șablon
  • Șabloanele de membri permit operații în diferite instanțieri ale aceluiași șablon de clasă

Test — Verifică-ți cunoștințele

(15 XP)

1. Având în vedere`template <typename T, typename Container = std::deque<T>> class Stack`, care este tipul de`Stack<int>`?

2. Ce este CTAD (C++17)?

3. Când vă specializați un șablon de clasă, ce se întâmplă cu membrii șablonului principal?