← Kursa Dön
📄 Text · 15 min

Fonksiyon Şablonları

Diyelim ki iki int değerin büyüğünü dönen bir fonksiyon yazdın. Sonra aynı şeyi double için de istiyorsun. Sonra std::string için de. Her seferinde aynı mantığı farklı tiplerle tekrar mı yazacaksın?

Hayır. C++ sana template (şablon) veriyor. Bir kere yaz, her tip için çalışsın.


Template Nedir?

Template, derleyiciye verdiğin bir kalıp. "Ben sana bir fonksiyonun genel yapısını veriyorum. Tipi sen doldur" diyorsun.

Analoji: Kurabiye kalıbı. Kalıbın şekli hep aynı — yıldız. Ama hamuru değiştirebilirsin: çikolatalı, tarçınlı, vanilyalı... Kalıp (template) aynı, malzeme (tip) farklı.

Template olmadan:

int maxValue(int a, int b) {
    return (a > b) ? a : b;
}

double maxValue(double a, double b) {
    return (a > b) ? a : b;
}

std::string maxValue(const std::string& a, const std::string& b) {
    return (a > b) ? a : b;
}

Üç fonksiyon, aynı mantık, sadece tipler farklı. Bu, DRY (Don't Repeat Yourself) ilkesine aykırı.

Template ile:

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

Tek bir tanım, tüm tipler için çalışır. Derleyici, kullandığın her tip için otomatik olarak bir versiyon üretir.


Template Söz Dizimi

template<typename T>
T fonksiyonAdi(T parametre1, T parametre2) {
    // T tipini kullan
}
  • template — "Bu bir şablon" demek

  • <typename T>T bir tip parametresi. Her tip olabilir: int, double, std::string, kendi sınıfın...

  • typename yerine class da yazabilirsin — ikisi tamamen aynı anlama gelir

template<class T>    // Aynı şey
template<typename T> // Aynı şey — typename daha modern

İlk Örnek: swap

template<typename T>
void mySwap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5, y = 10;
    mySwap(x, y);
    std::cout << x << ", " << y << "\n";  // 10, 5
    
    std::string s1 = "hello", s2 = "world";
    mySwap(s1, s2);
    std::cout << s1 << ", " << s2 << "\n";  // world, hello
}

Derleyici, mySwap(x, y) gördüğünde T = int ile bir versiyon üretir. mySwap(s1, s2) gördüğünde T = std::string ile başka bir versiyon üretir. Bu sürece template instantiation denir.


Otomatik Tür Çıkarımı (Type Deduction)

Template fonksiyonunu çağırırken tipi açıkça belirtmene genellikle gerek yok. Derleyici argümanlardan otomatik olarak çıkarır:

template<typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    auto r1 = add(3, 5);         // T = int (otomatik)
    auto r2 = add(3.14, 2.71);   // T = double (otomatik)
    
    // Açıkça belirtmek de mümkün:
    auto r3 = add<double>(3, 2.5);  // T = double (açık)
}

Ama bazen derleyici çıkaramaz:

template<typename T>
T create() {  // Parametresiz — derleyici T'yi nereden çıkaracak?
    return T{};
}

int main() {
    // auto x = create();      // HATA! T ne?
    auto x = create<int>();    // OK — açıkça belirt
    auto y = create<std::string>();  // OK
}

Parametre listesinde T kullanılmıyorsa, derleyici tipi çıkaramaz. Bu durumda açıkça belirtmelisin.

Tür Çıkarımında Çakışma

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

int main() {
    // maxValue(3, 2.5);   // HATA! T hem int hem double olamaz
    maxValue<double>(3, 2.5);  // OK — açıkça double
}

İki argüman farklı tipte olunca ve tek bir T parametresi varsa, derleyici hangisini seçeceğini bilemez. Çözüm: ya açıkça belirt, ya da iki ayrı template parametresi kullan.


Birden Fazla Template Parametresi

Tek bir T yetmediğinde birden fazla tip parametresi kullanabilirsin:

template<typename T, typename U>
auto add(T a, U b) {
    return a + b;  // auto dönüş tipi — derleyici çıkarır
}

int main() {
    auto r1 = add(3, 2.5);       // int + double → double
    auto r2 = add(3.14, 10);     // double + int → double
    std::cout << r1 << "\n";     // 5.5
    std::cout << r2 << "\n";     // 13.14
}

Burada T ve U farklı tipler olabilir. auto dönüş tipi ile derleyici en uygun dönüş tipini belirler.

Farklı Tipleri Yazdıran Fonksiyon

template<typename T, typename U>
void printPair(const T& first, const U& second) {
    std::cout << "(" << first << ", " << second << ")\n";
}

int main() {
    printPair(42, "hello");       // (42, hello)
    printPair(3.14, true);        // (3.14, 1)
    printPair("name", std::string("Ali"));  // (name, Ali)
}

Non-Type Template Parametreleri

Template parametreleri sadece tip olmak zorunda değil. Değer de olabilir:

template<typename T, int N>
class Array {
    T data[N];
public:
    int size() const { return N; }
    
    T& operator[](int index) { return data[index]; }
    const T& operator[](int index) const { return data[index]; }
};

int main() {
    Array<int, 5> arr;
    arr[0] = 42;
    std::cout << arr.size() << "\n";  // 5
}

N bir tip değil, bir değer. Derleme zamanında bilinen bir sabit olmalı.

Fonksiyonlarda da kullanılabilir:

template<int N>
int power(int base) {
    int result = 1;
    for (int i = 0; i < N; i++) {
        result *= base;
    }
    return result;
}

int main() {
    std::cout << power<3>(2) << "\n";  // 2^3 = 8
    std::cout << power<4>(3) << "\n";  // 3^4 = 81
}

Non-type parametreler şunlar olabilir:

  • Integral tipler: int, long, char, bool, size_t, enum...

  • Pointer veya referans (belirli koşullarda)

  • C++20'den itibaren: floating-point tipler ve literal class tipler

💡 `std::array` standart kütüphanede tam olarak bu teknikle çalışır: std::array<int, 5>. Boyut derleme zamanında sabittir ve stack'te tahsis edilir.


Template Instantiation

Template bir fonksiyon veya sınıf değildir. Fonksiyon/sınıf üretmek için bir reçetedir. Derleyici, template'i kullandığın her farklı tip için gerçek kodu üretir:

template<typename T>
T square(T x) {
    return x * x;
}

int main() {
    square(5);      // → int square(int x) üretilir
    square(3.14);   // → double square(double x) üretilir
    // İki ayrı fonksiyon oluştu!
}

Bu sürece instantiation denir. Her farklı tip için ayrı bir fonksiyon binary'ye eklenir. Bu, template'lerin zero-cost abstraction olduğu anlamına gelir: çalışma zamanı maliyeti sıfır. Ama derleme süresi ve binary boyutu artabilir.

Açık (Explicit) Instantiation

Derleyiciye belirli tipler için template'i önceden üretmesini söyleyebilirsin:

// header.h
template<typename T>
T maxValue(T a, T b);

// source.cpp
template<typename T>
T maxValue(T a, T b) {
    return (a > b) ? a : b;
}

// Açık instantiation
template int maxValue<int>(int, int);
template double maxValue<double>(double, double);

Bu, büyük projelerde derleme süresini azaltmak için kullanılır.


Template vs Overloading

Ne zaman template, ne zaman overloading kullanmalısın?

Overloading Tercih Et:

Farklı tipler için farklı mantık gerekiyorsa:

void print(int x) {
    std::cout << "Integer: " << x << "\n";
}

void print(const std::string& s) {
    std::cout << "String: \"" << s << "\" (length: " << s.size() << ")\n";
}

void print(double x) {
    std::cout << std::fixed << std::setprecision(2) << x << "\n";
}

Her tip için davranış farklı — template burada mantıklı olmaz.

Template Tercih Et:

Aynı mantık, farklı tiplerle çalışıyorsa:

template<typename T>
T clamp(T value, T low, T high) {
    if (value < low) return low;
    if (value > high) return high;
    return value;
}

Mantık int, double, float, hatta < operatörü olan herhangi bir tip için aynı.

İkisini Birlikte Kullanmak

Template ve overload birlikte kullanılabilir. Derleyici overload resolution sırasında non-template fonksiyonları template'lere tercih eder:

template<typename T>
void process(T value) {
    std::cout << "Generic: " << value << "\n";
}

// Non-template overload — özel durum
void process(const char* str) {
    std::cout << "C-string: " << str << "\n";
}

int main() {
    process(42);          // Generic: 42 (template)
    process(3.14);        // Generic: 3.14 (template)
    process("hello");     // C-string: hello (non-template, tercih edilir)
}

💡 Kural: Non-template fonksiyon, aynı imzalı template'ten önce tercih edilir. Bu, belirli tipler için özel davranış tanımlamanın en basit yoludur.


Template Fonksiyonlarda Kısıtlamalar

Template herhangi bir tiple çalışır — ama sadece o tipte gerekli operasyonlar varsa:

template<typename T>
T maxValue(T a, T b) {
    return (a > b) ? a : b;  // T, > operatörünü desteklemeli
}

struct Point {
    int x, y;
};

int main() {
    // maxValue(Point{1,2}, Point{3,4});  
    // HATA! Point için > operatörü tanımlı değil
}

Hata mesajı genellikle uzun ve anlaşılması zor olur. C++20 ile gelen Concepts bunu çözer:

#include <concepts>

template<typename T>
requires std::totally_ordered<T>
T maxValue(T a, T b) {
    return (a > b) ? a : b;
}

Artık Point ile çağırırsan, daha anlaşılır bir hata mesajı alırsın: "T, totally_ordered concept'ini karşılamıyor."

Concepts konusu ileri seviye — şimdilik bunun varlığını bilmen yeterli. Ama requires gördüğünde ne anlama geldiğini anlarsın.


Pratik Örnek: Generic Find ve Utility Fonksiyonları

#include <iostream>
#include <vector>
#include <string>
#include <numeric>

template<typename Container, typename Value>
bool contains(const Container& container, const Value& target) {
    for (const auto& item : container) {
        if (item == target) return true;
    }
    return false;
}

template<typename Container>
auto sum(const Container& c) {
    using ValueType = typename Container::value_type;
    ValueType total{};
    for (const auto& item : c) {
        total += item;
    }
    return total;
}

template<typename Container>
auto average(const Container& c) {
    return static_cast<double>(sum(c)) / c.size();
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<std::string> words = {"hello", "world"};
    
    std::cout << std::boolalpha;
    std::cout << contains(numbers, 3) << "\n";       // true
    std::cout << contains(numbers, 10) << "\n";      // false
    std::cout << contains(words, std::string("hello")) << "\n";  // true
    
    std::cout << "Sum: " << sum(numbers) << "\n";       // 15
    std::cout << "Average: " << average(numbers) << "\n"; // 3
    
    std::vector<double> prices = {9.99, 14.50, 3.25};
    std::cout << "Total: $" << sum(prices) << "\n";       // 27.74
    std::cout << "Avg: $" << average(prices) << "\n";     // 9.24667
}

contains, sum, average — üç farklı generic utility fonksiyonu. Herhangi bir container tipi ve herhangi bir değer tipiyle çalışır. Bu, template'lerin günlük programlamadaki en yaygın kullanım şeklidir.

typename Container::value_type ile container'ın eleman tipine erişebilirsin. Standart kütüphanedeki tüm container'lar (vector, list, deque, set...) bu type alias'ı sağlar.


Header'da Tanımlama Zorunluluğu

Template fonksiyonlar genellikle header dosyasında tanımlanır (sadece bildirimi değil, gövdesi de):

// utils.h
#pragma once

template<typename T>
T maxValue(T a, T b) {
    return (a > b) ? a : b;  // Gövde header'da!
}

Neden? Çünkü derleyici, template'i kullanıldığı yerde instantiate etmesi gerekir. .cpp dosyasında tanımlarsan ve başka bir .cpp dosyasından kullanırsan, derleyici tanımı göremez ve linker hatası alırsın.

Linker hatası genellikle şöyle görünür:

undefined reference to `int maxValue<int>(int, int)'

Bu, derleyicinin maxValue<int> için kodu üretemediği anlamına gelir — çünkü tanımı görememiş.

⚠️ Kural: Template fonksiyonlarını header dosyasında tanımla. Bu, normal fonksiyonlardaki "declaration header'da, definition .cpp'de" kuralının istisnasıdır.

Bu kuralın istisnası: explicit instantiation kullanıyorsan, tanımı .cpp'de tutabilirsin. Ama bu nadiren tercih edilir.

Proje Düzeni Örneği

project/
├── include/
│   └── math_utils.h    // Template tanımları burada
├── src/
│   └── main.cpp        // Template'leri kullanır
└── CMakeLists.txt
// include/math_utils.h
#pragma once
#include <algorithm>

template<typename T>
T clamp(T value, T low, T high) {
    return std::max(low, std::min(value, high));
}

template<typename T>
T lerp(T a, T b, double t) {
    return a + static_cast<T>((b - a) * t);
}
// src/main.cpp
#include "math_utils.h"
#include <iostream>

int main() {
    std::cout << clamp(15, 0, 10) << "\n";     // 10
    std::cout << lerp(0.0, 100.0, 0.3) << "\n"; // 30
}

auto Return Type ve Trailing Return

C++14'ten itibaren template fonksiyonlarda dönüş tipini auto bırakabilirsin:

// C++14 — auto return
template<typename T, typename U>
auto multiply(T a, U b) {
    return a * b;  // Dönüş tipi otomatik çıkarılır
}

// C++11 — trailing return type
template<typename T, typename U>
auto multiply11(T a, U b) -> decltype(a * b) {
    return a * b;
}

auto daha temiz ama decltype dönüş tipini açıkça gösterir — API documentation açısından bazen faydalı.

int main() {
    auto r1 = multiply(3, 2.5);    // double (int * double = double)
    auto r2 = multiply(3, 4);       // int (int * int = int)
    
    std::cout << r1 << "\n";  // 7.5
    std::cout << r2 << "\n";  // 12
}

Template ve constexpr

Template'ler constexpr ile birleştiğinde derleme zamanı hesaplama yapabilirsin:

template<int N>
constexpr int factorial() {
    if constexpr (N <= 1) return 1;
    else return N * factorial<N - 1>();
}

int main() {
    constexpr int f5 = factorial<5>();   // 120 — derleme zamanında hesaplandı
    constexpr int f10 = factorial<10>(); // 3628800
    
    static_assert(factorial<5>() == 120, "Factorial is wrong");
    
    std::cout << f5 << "\n";   // 120
    std::cout << f10 << "\n";  // 3628800
}

constexpr + template = derleme zamanı metaprogramlama. Çalışma zamanında hiçbir hesaplama yapılmaz. Sonuç binary'ye sabit olarak gömülür.

template<typename T, int N>
constexpr T power(T base) {
    T result = 1;
    for (int i = 0; i < N; i++) {
        result *= base;
    }
    return result;
}

int main() {
    constexpr auto p = power<double, 3>(2.0);  // 8.0 — derleme zamanında
    std::cout << p << "\n";
}

Template ile Callback Pattern

Template'ler callable (çağrılabilir) nesneleri parametre olarak almakta harikadır:

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

template<typename Container, typename Predicate>
int countIf(const Container& c, Predicate pred) {
    int count = 0;
    for (const auto& item : c) {
        if (pred(item)) count++;
    }
    return count;
}

template<typename Container, typename Func>
void forEach(const Container& c, Func func) {
    for (const auto& item : c) {
        func(item);
    }
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    
    // Lambda ile
    int evens = countIf(numbers, [](int n) { return n % 2 == 0; });
    std::cout << "Evens: " << evens << "\n";  // 5
    
    int bigOnes = countIf(numbers, [](int n) { return n > 5; });
    std::cout << "Greater than 5: " << bigOnes << "\n";  // 5
    
    // forEach ile yazdırma
    forEach(numbers, [](int n) {
        std::cout << n * n << " ";
    });
    std::cout << "\n";  // 1 4 9 16 25 36 49 64 81 100
    
    // String container ile de çalışır
    std::vector<std::string> words = {"hello", "hi", "hey", "howdy", "sup"};
    int shortWords = countIf(words, [](const std::string& s) {
        return s.size() <= 3;
    });
    std::cout << "Short words: " << shortWords << "\n";  // 3
}

Predicate ve Func herhangi bir callable olabilir: lambda, fonksiyon pointer'ı, functor... Template derleyicinin hepsini kabul etmesini ve doğru kodu üretmesini sağlar.

Bu pattern, STL algoritmalarının (std::sort, std::find_if, std::transform) temelini oluşturur.

Kendi STL-Tarzı Algoritman

STL'in nasıl çalıştığını anlamak için basit bir transform fonksiyonu yazalım:

template<typename InputIt, typename OutputIt, typename Func>
OutputIt myTransform(InputIt first, InputIt last, OutputIt dest, Func func) {
    while (first != last) {
        *dest = func(*first);
        ++first;
        ++dest;
    }
    return dest;
}

int main() {
    std::vector<int> input = {1, 2, 3, 4, 5};
    std::vector<int> output(input.size());
    
    myTransform(input.begin(), input.end(), output.begin(),
                [](int x) { return x * x; });
    
    for (int x : output) std::cout << x << " ";  // 1 4 9 16 25
    std::cout << "\n";
    
    // String ile de çalışır
    std::vector<std::string> words = {"hello", "world"};
    std::vector<size_t> lengths(words.size());
    
    myTransform(words.begin(), words.end(), lengths.begin(),
                [](const std::string& s) { return s.size(); });
    
    for (size_t len : lengths) std::cout << len << " ";  // 5 5
}

Üç farklı template parametresi: InputIt (giriş iterator), OutputIt (çıkış iterator), Func (dönüşüm fonksiyonu). Iterator'ların tipi ne olursa olsun çalışır — vector, list, deque, raw array...


Template'lerin Avantajları ve Dezavantajları

Avantajlar

  1. Kod tekrarını önler: Aynı mantığı farklı tiplerle kullan

  2. Tip güvenliği: Derleme zamanında tip kontrolü yapılır

  3. Sıfır çalışma zamanı maliyeti: Her instantiation doğrudan fonksiyon çağrısı üretir, vtable indirection yok

  4. Inline optimizasyon: Derleyici template fonksiyonları agresif şekilde inline edebilir

Dezavantajlar

  1. Derleme süresi: Her instantiation derleyicinin yeni kod üretmesi demek — büyük projelerde derleme yavaşlar

  2. Binary boyutu: sort<int>, sort<double>, sort<string> → üç ayrı fonksiyon binary'ye eklenir (code bloat)

  3. Hata mesajları: Template hatası mesajları genellikle uzun ve okunması zor (C++20 Concepts bunu iyileştirir)

  4. Header-only zorunluluğu: Tanım header'da olmalı — encapsulation zorlaşır

  5. Debug zorluğu: Template kodu debug ederken step-through karmaşıklaşabilir

// Tipik template hata mesajı (korkunç):
// error: no matching function for call to 'sort'
// note: candidate template ignored: substitution failure
//   [with T = MyClass]: no match for 'operator<'
//   in 'a < b'
//   ...50 satır daha...

// C++20 Concepts ile:
// error: MyClass does not satisfy 'sortable'
// note: 'operator<' is not defined

💡 İpucu: Template hata mesajıyla karşılaşınca, en son satırdan başlayarak yukarı oku. Genellikle asıl hata en altta belirtilir.


Özet

  • Template, derleyiciye verilen bir kalıptır — "tipi sen doldur" mantığıyla çalışır. Kod tekrarını önler ve type-safe generic programlamayı sağlar.

  • template<typename T> söz dizimi ile tanımlanır. typename yerine class da yazılabilir — ikisi aynıdır.

  • Derleyici çoğu zaman otomatik tür çıkarımı yapar. Çıkaramadığı durumlarda func<Type>(args) şeklinde açıkça belirtilir.

  • Non-type parametreler (int N gibi) derleme zamanı sabitleridir ve array boyutu gibi durumlarda kullanılır.

  • Template vs overloading: aynı mantık, farklı tip → template. Farklı mantık, farklı tip → overloading. İkisi birlikte de kullanılabilir.

  • Template fonksiyonlar header dosyasında tanımlanmalıdır. constexpr ile birleştirildiğinde derleme zamanı hesaplama gücü kazanır.