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>—Tbir tip parametresi. Her tip olabilir:int,double,std::string, kendi sınıfın...typenameyerineclassda 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
Kod tekrarını önler: Aynı mantığı farklı tiplerle kullan
Tip güvenliği: Derleme zamanında tip kontrolü yapılır
Sıfır çalışma zamanı maliyeti: Her instantiation doğrudan fonksiyon çağrısı üretir, vtable indirection yok
Inline optimizasyon: Derleyici template fonksiyonları agresif şekilde inline edebilir
Dezavantajlar
Derleme süresi: Her instantiation derleyicinin yeni kod üretmesi demek — büyük projelerde derleme yavaşlar
Binary boyutu:
sort<int>,sort<double>,sort<string>→ üç ayrı fonksiyon binary'ye eklenir (code bloat)Hata mesajları: Template hatası mesajları genellikle uzun ve okunması zor (C++20 Concepts bunu iyileştirir)
Header-only zorunluluğu: Tanım header'da olmalı — encapsulation zorlaşır
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.typenameyerineclassda 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 Ngibi) 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.
constexprile birleştirildiğinde derleme zamanı hesaplama gücü kazanır.
AI Asistan
Sorularını yanıtlamaya hazır