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/.cppayrı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ırKendi 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.elementsyerinethis->elementsyazmalı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.usingalias ile uzun template isimlerini kısaltabilirsin.
AI Asistan
Sorularını yanıtlamaya hazır