← Kursa Dön
📄 Text · 15 min

Sınıf Şablonları

Fonksiyon şablonlarını gördük — tip bağımsız fonksiyonlar yazabildik. Aynı mantık sınıflar için de geçerli. Bir Stack veri yapısı istiyorsun: int için de çalışsın, string için de, double için de. Her tip için ayrı sınıf mı yazacaksın?

Tabii ki hayır. Class template ile bir kere yaz, her tip için kullan.


Class Template Nedir?

Class template, bir sınıfın tip parametresi ile tanımlanmasıdır. Aynı yapı, farklı tiplerle kullanılabilir.

Analoji: Buzdolabı. Buzdolabının kendisi hep aynı yapıda — raflar, çekmeceler, kapak. Ama içine ne koyacağın sana bağlı: meyve, süt, et... Buzdolabı bir template, içindekiler tip parametresi.

template<typename T>
class Box {
    T value;
public:
    Box(const T& v) : value(v) {}
    
    T getValue() const { return value; }
    void setValue(const T& v) { value = v; }
};

int main() {
    Box<int> intBox(42);
    Box<std::string> strBox("Hello");
    Box<double> dblBox(3.14);
    
    std::cout << intBox.getValue() << "\n";  // 42
    std::cout << strBox.getValue() << "\n";  // Hello
    std::cout << dblBox.getValue() << "\n";  // 3.14
}

Box<int> ve Box<std::string> farklı tiplerdir. Derleyici her biri için ayrı sınıf üretir.


Class Template Tanımlama

Temel söz dizimi:

template<typename T>
class ClassName {
    T member;
public:
    ClassName(const T& val);
    T getMember() const;
    void setMember(const T& val);
};

T sınıf boyunca kullanılabilir: üye değişken tipi, fonksiyon parametre tipi, dönüş tipi...

Tam Bir Örnek: Pair

template<typename T>
class Pair {
    T first;
    T second;
public:
    Pair(const T& f, const T& s) : first(f), second(s) {}
    
    T getFirst() const { return first; }
    T getSecond() const { return second; }
    
    void swap() {
        T temp = first;
        first = second;
        second = temp;
    }
    
    void print() const {
        std::cout << "(" << first << ", " << second << ")\n";
    }
};

int main() {
    Pair<int> p1(10, 20);
    p1.print();   // (10, 20)
    p1.swap();
    p1.print();   // (20, 10)
    
    Pair<std::string> p2("hello", "world");
    p2.print();   // (hello, world)
}

Üye Fonksiyonları Sınıf Dışında Tanımlama

Büyük sınıflarda üye fonksiyonları sınıf gövdesi dışında tanımlamak okunabilirliği artırır. Ama class template'lerde söz dizimi biraz farklı:

template<typename T>
class Stack {
    std::vector<T> elements;
public:
    void push(const T& value);
    void pop();
    T top() const;
    bool empty() const;
    int size() const;
};

// Her fonksiyon için template<typename T> tekrarlanmalı
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 is empty");
    }
    elements.pop_back();
}

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

template<typename T>
bool Stack<T>::empty() const {
    return elements.empty();
}

template<typename T>
int Stack<T>::size() const {
    return elements.size();
}

Her fonksiyonun önünde template<typename T> olmalı ve sınıf adı Stack<T>:: şeklinde yazılmalı.

Kullanımı:

int main() {
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    intStack.push(30);
    
    while (!intStack.empty()) {
        std::cout << intStack.top() << " ";
        intStack.pop();
    }
    // 30 20 10
    
    Stack<std::string> strStack;
    strStack.push("first");
    strStack.push("second");
    std::cout << strStack.top() << "\n";  // second
}

💡 Hatırlatma: Template sınıfların tüm tanımları (fonksiyon gövdeleri dahil) header dosyasında olmalıdır. Normal sınıflardaki .h/.cpp ayrımı burada geçerli değildir.


Birden Fazla Tür Parametresi

Bir class template birden fazla tip parametresi alabilir:

template<typename K, typename V>
class KeyValue {
    K key;
    V value;
public:
    KeyValue(const K& k, const V& v) : key(k), value(v) {}
    
    K getKey() const { return key; }
    V getValue() const { return value; }
    
    void print() const {
        std::cout << key << " → " << value << "\n";
    }
};

int main() {
    KeyValue<std::string, int> age("Ali", 25);
    age.print();  // Ali → 25
    
    KeyValue<int, std::string> lookup(404, "Not Found");
    lookup.print();  // 404 → Not Found
    
    KeyValue<std::string, double> price("Coffee", 4.99);
    price.print();  // Coffee → 4.99
}

std::map ve std::pair tam olarak bu mantıkla çalışır: std::pair<std::string, int>.


Default Template Argümanları

Template parametrelerine varsayılan değer verebilirsin:

template<typename T = int, typename Container = std::vector<T>>
class Stack {
    Container elements;
public:
    void push(const T& value) {
        elements.push_back(value);
    }
    
    T top() const {
        return elements.back();
    }
    
    void pop() {
        elements.pop_back();
    }
    
    bool empty() const {
        return elements.empty();
    }
};

int main() {
    Stack<> s1;                    // T = int, Container = vector<int>
    Stack<double> s2;              // T = double, Container = vector<double>
    Stack<std::string, std::deque<std::string>> s3;  // Özel container
    
    s1.push(42);
    s2.push(3.14);
    s3.push("hello");
}

Stack<> yazımında <> gerekli — derleyiciye bunun bir template olduğunu söyler. Varsayılan argümanlar sağdan sola verilir (fonksiyon parametreleri gibi).

Standart Kütüphanede Default Argümanlar

Standart kütüphanedeki birçok container default argüman kullanır:

// std::vector'ün gerçek tanımı:
template<typename T, typename Allocator = std::allocator<T>>
class vector;

// Bu yüzden sadece std::vector<int> yazabiliyorsun
// Allocator'ı özelleştirmek istersen: std::vector<int, MyAllocator<int>>

Template Sınıf ve Exception Safety

Template sınıflar tasarlarken exception safety düşünmek önemli:

template<typename T>
class SafeStack {
    T* data;
    int capacity;
    int topIndex;
    
public:
    SafeStack(int cap = 16) 
        : data(new T[cap]), capacity(cap), topIndex(-1) {}
    
    ~SafeStack() { delete[] data; }
    
    // Copy constructor
    SafeStack(const SafeStack& other)
        : data(new T[other.capacity]), 
          capacity(other.capacity), 
          topIndex(other.topIndex) {
        for (int i = 0; i <= topIndex; i++) {
            data[i] = other.data[i];
        }
    }
    
    // Copy assignment (copy-and-swap idiom)
    SafeStack& operator=(SafeStack other) {
        std::swap(data, other.data);
        std::swap(capacity, other.capacity);
        std::swap(topIndex, other.topIndex);
        return *this;
    }
    
    void push(const T& value) {
        if (topIndex + 1 >= capacity) {
            // Büyüt — strong exception guarantee
            int newCap = capacity * 2;
            T* newData = new T[newCap];
            for (int i = 0; i <= topIndex; i++) {
                newData[i] = data[i];
            }
            delete[] data;
            data = newData;
            capacity = newCap;
        }
        data[++topIndex] = value;
    }
    
    T top() const {
        if (topIndex < 0) throw std::underflow_error("Empty stack");
        return data[topIndex];
    }
    
    void pop() {
        if (topIndex < 0) throw std::underflow_error("Empty stack");
        topIndex--;
    }
    
    bool empty() const { return topIndex < 0; }
};

Bu implementasyon copy-and-swap idiom kullanır — assignment operatöründe exception atılsa bile nesne tutarlı kalır. Template sınıf yazdığında Rule of Three/Five'ı unutma.


CTAD — Class Template Argument Deduction (C++17)

C++17 ile birlikte, bazı durumlarda tip parametresini belirtmene gerek kalmaz. Derleyici constructor argümanlarından çıkarabilir:

// C++17 öncesi
std::pair<int, double> p1(42, 3.14);

// C++17 ile — CTAD
std::pair p2(42, 3.14);     // pair<int, double> çıkarılır
std::vector v{1, 2, 3, 4};  // vector<int> çıkarılır

Kendi sınıflarında da çalışır:

template<typename T>
class Box {
    T value;
public:
    Box(const T& v) : value(v) {}
    T get() const { return value; }
};

int main() {
    Box box(42);           // Box<int> — C++17 CTAD
    Box strBox("hello"s);  // Box<string>
}

💡 Not: CTAD her zaman çalışmaz. Karmaşık constructor'larda veya varsayılan parametrelerde sorun çıkabilir. Emin değilsen tipi açıkça yaz.


Stack<T> — Tam Örnek

Öğrendiklerini birleştirerek tam bir generic Stack implementasyonu yapalım:

#include <iostream>
#include <vector>
#include <stdexcept>
#include <string>

template<typename T>
class Stack {
    std::vector<T> data;
    
public:
    // Element ekle
    void push(const T& value) {
        data.push_back(value);
    }
    
    // Move semantics desteği
    void push(T&& value) {
        data.push_back(std::move(value));
    }
    
    // Üstteki elementi kaldır
    void pop() {
        if (data.empty()) {
            throw std::underflow_error("Stack underflow: pop on empty stack");
        }
        data.pop_back();
    }
    
    // Üstteki elementi oku
    const T& top() const {
        if (data.empty()) {
            throw std::underflow_error("Stack underflow: top on empty stack");
        }
        return data.back();
    }
    
    // Boş mu?
    bool empty() const { return data.empty(); }
    
    // Eleman sayısı
    std::size_t size() const { return data.size(); }
    
    // Tüm elemanları yazdır (debug için)
    void print() const {
        std::cout << "Stack [top → bottom]: ";
        for (auto it = data.rbegin(); it != data.rend(); ++it) {
            std::cout << *it << " ";
        }
        std::cout << "\n";
    }
};

Kullanım:

int main() {
    // Integer stack
    Stack<int> intStack;
    intStack.push(10);
    intStack.push(20);
    intStack.push(30);
    intStack.print();  // Stack [top → bottom]: 30 20 10
    
    std::cout << "Top: " << intStack.top() << "\n";  // 30
    intStack.pop();
    intStack.print();  // Stack [top → bottom]: 20 10
    
    // String stack
    Stack<std::string> strStack;
    strStack.push("first");
    strStack.push("second");
    strStack.push("third");
    strStack.print();  // Stack [top → bottom]: third second first
    
    // Boş stack'te pop — exception
    Stack<double> emptyStack;
    try {
        emptyStack.pop();
    } catch (const std::underflow_error& e) {
        std::cout << "Error: " << e.what() << "\n";
    }
}

Bu basit Stack, std::vector üzerine inşa edilmiş bir adapter. Gerçek std::stack da tam olarak böyle çalışır — std::deque veya std::vector üzerine bir adaptördür.


Class Template ve Kalıtım

Template sınıflar kalıtımla da kullanılabilir:

// Template base class
template<typename T>
class Container {
protected:
    std::vector<T> elements;
public:
    void add(const T& item) {
        elements.push_back(item);
    }
    std::size_t size() const {
        return elements.size();
    }
};

// Template derived class
template<typename T>
class SortedContainer : public Container<T> {
public:
    void addSorted(const T& item) {
        this->elements.push_back(item);
        std::sort(this->elements.begin(), this->elements.end());
    }
    
    void print() const {
        for (const auto& e : this->elements) {
            std::cout << e << " ";
        }
        std::cout << "\n";
    }
};

int main() {
    SortedContainer<int> sc;
    sc.addSorted(30);
    sc.addSorted(10);
    sc.addSorted(20);
    sc.print();  // 10 20 30
}

⚠️ Dikkat: Template base class'ın üyelerine erişirken this-> kullanman gerekir. elements yerine this->elements yazmalısın. Bu, C++'ın "two-phase lookup" kuralından kaynaklanır — this-> olmadan derleyici base class üyelerini göremeyebilir.


Static Üyeler ve Template

Her template instantiation ayrı bir sınıf oluşturur. Bu, static üyeler için de geçerlidir:

template<typename T>
class Counter {
    static int count;
public:
    Counter() { count++; }
    ~Counter() { count--; }
    static int getCount() { return count; }
};

template<typename T>
int Counter<T>::count = 0;

int main() {
    Counter<int> a, b, c;
    Counter<double> x, y;
    
    std::cout << Counter<int>::getCount() << "\n";     // 3
    std::cout << Counter<double>::getCount() << "\n";   // 2
}

Counter<int> ve Counter<double> farklı sınıflar olduğu için farklı count değişkenleri var.


Nested Template ve Friend

Template sınıflar içinde template fonksiyonlar veya friend bildirimleri olabilir:

template<typename T>
class Box {
    T value;
public:
    Box(const T& v) : value(v) {}
    
    // Friend fonksiyon — << operatörü
    friend std::ostream& operator<<(std::ostream& os, const Box<T>& box) {
        os << "[" << box.value << "]";
        return os;
    }
    
    // Farklı tipte Box ile karşılaştırma
    template<typename U>
    bool hasSameSize(const Box<U>& other) const {
        return sizeof(value) == sizeof(other.value);
    }
};

int main() {
    Box<int> b1(42);
    Box<double> b2(3.14);
    
    std::cout << b1 << "\n";  // [42]
    std::cout << b2 << "\n";  // [3.14]
}

Pratik Örnek: Optional<T>

std::optional benzeri basit bir generic container yazalım. Bu, bir değerin var olup olmadığını temsil eder:

#include <iostream>
#include <stdexcept>
#include <string>

template<typename T>
class Optional {
    T value;
    bool hasVal;
    
public:
    // Boş optional
    Optional() : value{}, hasVal(false) {}
    
    // Değerli optional
    Optional(const T& v) : value(v), hasVal(true) {}
    
    // Değer var mı?
    bool hasValue() const { return hasVal; }
    explicit operator bool() const { return hasVal; }
    
    // Değere eriş
    const T& getValue() const {
        if (!hasVal) throw std::runtime_error("No value");
        return value;
    }
    
    // Değer yoksa varsayılan döndür
    T valueOr(const T& defaultVal) const {
        return hasVal ? value : defaultVal;
    }
    
    // Değeri sıfırla
    void reset() { hasVal = false; }
    
    // Değer ata
    void setValue(const T& v) {
        value = v;
        hasVal = true;
    }
};

int main() {
    Optional<int> maybeAge;
    std::cout << "Has age? " << std::boolalpha << maybeAge.hasValue() << "\n";  // false
    std::cout << "Age: " << maybeAge.valueOr(-1) << "\n";  // -1
    
    maybeAge.setValue(25);
    std::cout << "Age: " << maybeAge.getValue() << "\n";  // 25
    
    Optional<std::string> maybeName("Ali");
    if (maybeName) {
        std::cout << "Name: " << maybeName.getValue() << "\n";  // Ali
    }
    
    Optional<double> empty;
    try {
        empty.getValue();  // Exception!
    } catch (const std::runtime_error& e) {
        std::cout << "Error: " << e.what() << "\n";
    }
}

Bu basitleştirilmiş bir versiyon. Gerçek std::optional (C++17) move semantics, in-place construction ve daha fazlasını destekler. Ama mantık aynı: "değer var mı yok mu" durumunu tip güvenli şekilde ifade et.


Type Alias ve Template

Template sınıfları daha kısa isimlerle kullanmak için type alias tanımlayabilirsin:

template<typename T>
class Matrix {
    std::vector<std::vector<T>> data;
    int rows, cols;
public:
    Matrix(int r, int c) : rows(r), cols(c), data(r, std::vector<T>(c)) {}
    
    T& at(int r, int c) { return data[r][c]; }
    const T& at(int r, int c) const { return data[r][c]; }
    int getRows() const { return rows; }
    int getCols() const { return cols; }
};

// Type alias — kısa isimler
using IntMatrix = Matrix<int>;
using FloatMatrix = Matrix<float>;
using DoubleMatrix = Matrix<double>;

// Template alias — parametre korunur
template<typename T>
using Vec = std::vector<T>;

template<typename T>
using SquareMatrix = Matrix<T>;  // Anlam katıyor

int main() {
    IntMatrix m(3, 3);
    m.at(0, 0) = 1;
    m.at(1, 1) = 5;
    m.at(2, 2) = 9;
    
    Vec<int> numbers = {1, 2, 3};        // vector<int>
    Vec<std::string> words = {"hello"};   // vector<string>
}

using ile type alias tanımlamak hem okunabilirliği artırır hem de uzun template isimlerini kısaltır. typedef da kullanılabilir ama using daha modern ve template alias'ları destekler.


Template Sınıf ile Operator Overloading

Template sınıflarda operator overloading yaygın bir ihtiyaçtır:

template<typename T>
class Vec2 {
public:
    T x, y;
    
    Vec2(T x = 0, T y = 0) : x(x), y(y) {}
    
    // Toplama
    Vec2 operator+(const Vec2& other) const {
        return Vec2(x + other.x, y + other.y);
    }
    
    // Çıkarma
    Vec2 operator-(const Vec2& other) const {
        return Vec2(x - other.x, y - other.y);
    }
    
    // Skaler çarpma
    Vec2 operator*(T scalar) const {
        return Vec2(x * scalar, y * scalar);
    }
    
    // Eşitlik
    bool operator==(const Vec2& other) const {
        return x == other.x && y == other.y;
    }
    
    // Yazdırma
    friend std::ostream& operator<<(std::ostream& os, const Vec2& v) {
        os << "(" << v.x << ", " << v.y << ")";
        return os;
    }
};

int main() {
    Vec2<double> a(1.0, 2.0);
    Vec2<double> b(3.0, 4.0);
    
    auto c = a + b;
    auto d = a * 2.0;
    
    std::cout << "a + b = " << c << "\n";  // (4, 6)
    std::cout << "a * 2 = " << d << "\n";  // (2, 4)
    
    Vec2<int> p1(1, 2), p2(1, 2);
    std::cout << std::boolalpha << (p1 == p2) << "\n";  // true
}

Template sınıflarda operator overloading normal sınıflardaki ile aynıdır. Tek fark: friend fonksiyonları sınıf gövdesinde tanımlamak genellikle daha pratiktir (template ile friend tanımı sınıf dışında karmaşıklaşabilir).


Template Sınıf Tasarım İpuçları

Kendi template sınıfını yazarken dikkat etmen gereken noktalar:

1. Minimal arayüz: Sadece gerçekten generic olması gereken fonksiyonları template'e koy. Tip bağımsız yardımcı fonksiyonları non-template base class'ta tut.

// İYİ — tip-bağımsız kısmı ayır
class ContainerBase {
protected:
    std::size_t count = 0;
public:
    std::size_t size() const { return count; }
    bool empty() const { return count == 0; }
};

template<typename T>
class Container : public ContainerBase {
    std::vector<T> data;
public:
    void add(const T& item) { data.push_back(item); count++; }
    const T& get(int i) const { return data[i]; }
};

2. const doğruluğu: Getter fonksiyonlarını const yap. Hem const hem non-const erişim sağla.

3. Move semantics: Template sınıfın move constructor ve move assignment'ı desteklediğinden emin ol. = default genellikle yeterlidir.

4. noexcept: Move operasyonlarını noexcept işaretle. std::vector gibi container'lar, elemanları taşırken noexcept garantisi ararlar.


Özet

  • Class template, tip parametresi alan sınıf tanımıdır. template<typename T> class ClassName {...} söz dizimi ile tanımlanır.

  • Üye fonksiyonlar sınıf dışında tanımlanırken her birinin önüne template<typename T> eklenir ve sınıf adı ClassName<T>:: olarak yazılır.

  • Birden fazla tip parametresi (template<typename K, typename V>) ile key-value gibi yapılar oluşturulabilir.

  • Default template argümanları sağdan sola verilir ve kullanıcının her seferinde tip belirtmesini engellerler.

  • C++17 CTAD sayesinde bazı durumlarda tip parametresi otomatik çıkarılır: std::pair p(1, 2.0);

  • Template sınıfların tüm tanımları header dosyasında olmalıdır ve her instantiation (Stack<int>, Stack<string>) farklı bir sınıf üretir. using alias ile uzun template isimlerini kısaltabilirsin.