Variadic Templates
Şimdiye kadar gördüğün template'lerde parametre sayısı sabitti: template<typename T> veya template<typename T, typename U>. Peki ya kaç tane tip geleceğini bilmiyorsan?
Bir print() fonksiyonu düşün: bazen 1, bazen 3, bazen 7 argüman alabilmeli. C'deki printf bunu yapıyordu ama tip güvenliği yoktu. C++11 ile gelen variadic template'ler bunu hem tip güvenli hem de zarif şekilde çözüyor.
Parameter Pack Nedir?
Variadic template, sıfır veya daha fazla tip parametresi alabilir. Üç nokta (...) ile ifade edilir:
template<typename... Args> // Args bir "parameter pack"
void myFunction(Args... args) { // args bir "function parameter pack"
// ...
}typename... Args— template parameter pack: sıfır veya daha fazla tipArgs... args— function parameter pack: o tiplerdeki argümanlar
Analoji: Valizin içine istediğin kadar eşya koy. Valiz (parameter pack) kaç eşya alacağını önceden bilmez. 1 tişört de koyabilirsin, 10 parça da. Önemli olan hepsini düzgün çıkarabilmek.
template<typename... Args>
void example(Args... args) {
// args: 0, 1, 2, ... herhangi sayıda argüman
}
int main() {
example(); // 0 argüman
example(1); // 1 argüman (int)
example(1, 2.0, "hello"); // 3 argüman (int, double, const char*)
example(1, 'a', 3.14, true, "test"); // 5 argüman
}sizeof...(Args)
Pack'teki eleman sayısını öğrenmek için sizeof... kullanılır:
template<typename... Args>
void countArgs(Args... args) {
std::cout << "Number of arguments: " << sizeof...(Args) << "\n";
// sizeof...(args) da aynı sonucu verir
}
int main() {
countArgs(); // 0
countArgs(1, 2, 3); // 3
countArgs("a", 1, 3.14, true); // 4
}sizeof...(Args) derleme zamanında hesaplanır — çalışma zamanı maliyeti sıfırdır.
Recursive Unpacking
C++11'de parameter pack'i açmanın (expand) klasik yolu recursive (özyinelemeli) yaklaşımdır. Paketten bir eleman çıkarırsın, işlersin, kalanını kendine geri verirsin:
#include <iostream>
// Base case — recursion'ın durma noktası
void print() {
std::cout << "\n";
}
// Recursive case — ilk elemanı al, kalanını tekrar çağır
template<typename T, typename... Rest>
void print(T first, Rest... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
}
print(rest...); // Kalan argümanlarla tekrar çağır
}
int main() {
print(1, 2.5, "hello", 'A', true);
// 1, 2.5, hello, A, 1
}Nasıl çalışır:
print(1, 2.5, "hello", 'A', true)
→ first=1, rest={2.5, "hello", 'A', true}
→ yazdır: 1,
→ print(2.5, "hello", 'A', true)
→ first=2.5, rest={"hello", 'A', true}
→ yazdır: 2.5,
→ print("hello", 'A', true)
→ first="hello", rest={'A', true}
→ yazdır: hello,
→ print('A', true)
→ first='A', rest={true}
→ yazdır: A,
→ print(true)
→ first=true, rest={}
→ yazdır: 1
→ print() ← base case, \n yazdırHer çağrıda pack bir eleman küçülür, sonunda boş pack kalır ve base case çağrılır.
💡 `if constexpr` (C++17): Derleme zamanında koşul değerlendirilir.
sizeof...(rest) > 0kontrolü derleme zamanında yapılır, çalışma zamanı maliyeti yoktur.
Fold Expressions (C++17)
Recursive yaklaşım çalışıyor ama uzun ve okunması zor. C++17 ile gelen fold expressions bunu dramatik şekilde basitleştiriyor:
// Recursive yerine fold expression
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // Fold expression
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << "\n"; // 15
std::cout << sum(1.5, 2.5, 3.0) << "\n"; // 7.0
}Tek satır. Base case yok. Recursion yok. Sadece (args + ...).
Fold Expression Türleri
Dört tür fold expression var:
// 1. Unary right fold: (pack op ...)
// Açılımı: (a1 op (a2 op (a3 op a4)))
template<typename... Args>
auto rightFold(Args... args) {
return (args + ...);
}
// 2. Unary left fold: (... op pack)
// Açılımı: (((a1 op a2) op a3) op a4)
template<typename... Args>
auto leftFold(Args... args) {
return (... + args);
}
// 3. Binary right fold: (pack op ... op init)
// Açılımı: (a1 op (a2 op (a3 op init)))
template<typename... Args>
auto rightFoldInit(Args... args) {
return (args + ... + 0); // init = 0
}
// 4. Binary left fold: (init op ... op pack)
// Açılımı: (((init op a1) op a2) op a3)
template<typename... Args>
auto leftFoldInit(Args... args) {
return (0 + ... + args); // init = 0
}Binary fold (init değerli) boş pack durumunda da çalışır. Unary fold boş pack ile hata verir (bazı operatörler hariç).
Fold ile Yazdırma
template<typename... Args>
void print(Args... args) {
((std::cout << args << " "), ...);
std::cout << "\n";
}
int main() {
print(1, 2.5, "hello", true);
// 1 2.5 hello 1
}Burada virgül operatörü ile fold yapılıyor: (expr, ...) her eleman için expr çalıştırılır.
Fold ile Mantıksal Operasyonlar
// Tüm argümanlar true mu?
template<typename... Args>
bool allTrue(Args... args) {
return (args && ...);
}
// En az biri true mu?
template<typename... Args>
bool anyTrue(Args... args) {
return (args || ...);
}
int main() {
std::cout << std::boolalpha;
std::cout << allTrue(true, true, true) << "\n"; // true
std::cout << allTrue(true, false, true) << "\n"; // false
std::cout << anyTrue(false, false, true) << "\n"; // true
}💡 Fold expressions, variadic template'lerin en önemli modernleşmesidir. C++17 kullanabiliyorsan recursive yaklaşım yerine her zaman fold tercih et.
print() Fonksiyonu — Tam Örnek
Farklı yaklaşımları karşılaştıralım:
#include <iostream>
#include <string>
// Yaklaşım 1: Recursive (C++11)
void printRecursive() { std::cout << "\n"; }
template<typename T, typename... Rest>
void printRecursive(T first, Rest... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) std::cout << ", ";
printRecursive(rest...);
}
// Yaklaşım 2: Fold expression (C++17)
template<typename... Args>
void printFold(Args... args) {
std::size_t n = 0;
((std::cout << (n++ ? ", " : "") << args), ...);
std::cout << "\n";
}
int main() {
printRecursive(1, "hello", 3.14, true);
// 1, hello, 3.14, 1
printFold(1, "hello", 3.14, true);
// 1, hello, 3.14, 1
}İkisi de aynı çıktıyı verir. Fold versiyonu daha kısa ve okunabilir.
Perfect Forwarding ile Variadic Templates
Variadic template'lerin en güçlü kullanım alanlarından biri perfect forwarding. Argümanları başka bir fonksiyona hiç kopyalamadan, olduğu gibi iletmek:
#include <iostream>
#include <memory>
#include <string>
class Widget {
std::string name;
int value;
public:
Widget(const std::string& n, int v) : name(n), value(v) {
std::cout << "Widget(" << name << ", " << value << ")\n";
}
};
// Basitleştirilmiş make_unique benzeri fonksiyon
template<typename T, typename... Args>
std::unique_ptr<T> create(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
int main() {
auto w = create<Widget>("MyWidget", 42);
// Widget(MyWidget, 42)
}Burada olan:
Args&&... args— universal reference parameter packstd::forward<Args>(args)...— her argümanı olduğu gibi ilet (lvalue ise lvalue, rvalue ise rvalue olarak)
Bu pattern, standart kütüphanede her yerde kullanılır:
std::make_unique<Widget>("name", 42); // Argümanları forward eder
std::make_shared<Widget>("name", 42); // Aynı pattern
std::vector<Widget> v;
v.emplace_back("name", 42); // Argümanları forward ederemplace_back, make_unique, make_shared — hepsi variadic template + perfect forwarding kullanır. Bu fonksiyonlar argümanları doğrudan constructor'a iletir, gereksiz kopya oluşturmaz.
⚠️ `std::forward` olmadan perfect forwarding çalışmaz.
std::forward<Args>(args)...yazmayı unutursan, tüm argümanlar lvalue olarak iletilir ve move semantics kaybolur.
Pack Expansion Detayları
... (ellipsis) bir pattern'ı expand eder. Pattern, pack'in her elemanına uygulanır:
template<typename... Args>
void examples(Args... args) {
// args... → a1, a2, a3
// &args... → &a1, &a2, &a3
// func(args)... → func(a1), func(a2), func(a3)
// std::forward<Args>(args)... → std::forward<A1>(a1), std::forward<A2>(a2), ...
}Daha somut bir örnek:
template<typename... Args>
auto makeVector(Args&&... args) {
// initializer list ile vector oluştur
using CommonType = std::common_type_t<Args...>;
return std::vector<CommonType>{std::forward<Args>(args)...};
}
int main() {
auto v = makeVector(1, 2, 3, 4, 5);
for (auto x : v) std::cout << x << " "; // 1 2 3 4 5
}Emplace ve Variadic: Nesneleri Yerinde Oluşturma
emplace_back gibi fonksiyonlar variadic template + perfect forwarding ile nesneyi doğrudan container içinde oluşturur:
#include <iostream>
#include <vector>
#include <string>
class Person {
std::string name;
int age;
public:
Person(const std::string& n, int a) : name(n), age(a) {
std::cout << "Person constructed: " << name << "\n";
}
Person(const Person& other) : name(other.name), age(other.age) {
std::cout << "Person COPIED: " << name << "\n";
}
Person(Person&& other) noexcept : name(std::move(other.name)), age(other.age) {
std::cout << "Person MOVED: " << name << "\n";
}
void print() const {
std::cout << name << " (" << age << ")\n";
}
};
int main() {
std::vector<Person> people;
people.reserve(3);
std::cout << "--- push_back ---\n";
people.push_back(Person("Ali", 25)); // Construct + Move
std::cout << "--- emplace_back ---\n";
people.emplace_back("Ayşe", 30); // Sadece construct (yerinde!)
}push_back geçici nesne oluşturup move eder. emplace_back argümanları doğrudan constructor'a forward eder — gereksiz move/copy yok. Bu fark büyük nesnelerde veya yoğun döngülerde önemli olabilir.
constexpr if ile Derleme Zamanı Dallanma
if constexpr (C++17), variadic template'lerde recursive durma koşulunu base case fonksiyonu olmadan yazmanı sağlar:
template<typename T, typename... Rest>
void printAll(T first, Rest... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
printAll(rest...); // Sadece rest boş değilse derlenir
} else {
std::cout << "\n";
}
}
int main() {
printAll(1, "two", 3.0);
// 1, two, 3
}if constexpr false olan dal derlenmez bile. Bu, base case overload'ını gereksiz kılar.
Pratik Örnek: Logger
Variadic template ile tip güvenli bir logger:
#include <iostream>
#include <sstream>
#include <string>
enum class LogLevel { INFO, WARNING, ERROR };
std::string levelToString(LogLevel level) {
switch (level) {
case LogLevel::INFO: return "INFO";
case LogLevel::WARNING: return "WARN";
case LogLevel::ERROR: return "ERROR";
}
return "UNKNOWN";
}
template<typename... Args>
void log(LogLevel level, Args&&... args) {
std::cout << "[" << levelToString(level) << "] ";
((std::cout << std::forward<Args>(args)), ...);
std::cout << "\n";
}
int main() {
log(LogLevel::INFO, "Server started on port ", 8080);
log(LogLevel::WARNING, "Memory usage: ", 85.5, "%");
log(LogLevel::ERROR, "Failed to open file: ", "data.txt",
" (errno=", 2, ")");
}Çıktı:
[INFO] Server started on port 8080
[WARN] Memory usage: 85.5%
[ERROR] Failed to open file: data.txt (errno=2)printf gibi format string yok. Her argüman kendi tipine göre yazdırılır. Tip güvenli, genişletilebilir ve derleme zamanında kontrol edilen bir çözüm.
Tuple ile İlişki
std::tuple variadic template'lerin klasik örneğidir:
#include <tuple>
#include <string>
#include <iostream>
int main() {
// std::tuple — farklı tiplerde değerler tutan konteyner
std::tuple<int, std::string, double> record(1, "Ali", 3.85);
std::cout << std::get<0>(record) << "\n"; // 1
std::cout << std::get<1>(record) << "\n"; // Ali
std::cout << std::get<2>(record) << "\n"; // 3.85
// C++17 structured bindings
auto [id, name, gpa] = record;
std::cout << name << " - GPA: " << gpa << "\n";
// make_tuple — variadic template kullanır
auto t = std::make_tuple(42, "hello", 3.14);
}std::tuple aslında variadic class template:
template<typename... Types>
class tuple;Variadic Class Templates
Variadic template'ler sadece fonksiyonlarda değil, sınıflarda da kullanılabilir:
#include <iostream>
#include <string>
// Basit type list
template<typename... Types>
struct TypeList {
static constexpr std::size_t size = sizeof...(Types);
};
int main() {
using MyTypes = TypeList<int, double, std::string>;
std::cout << "Size: " << MyTypes::size << "\n"; // 3
using Empty = TypeList<>;
std::cout << "Empty: " << Empty::size << "\n"; // 0
}Daha pratik bir örnek — basit bir event emitter:
#include <iostream>
#include <functional>
#include <vector>
template<typename... Args>
class Event {
std::vector<std::function<void(Args...)>> handlers;
public:
void subscribe(std::function<void(Args...)> handler) {
handlers.push_back(std::move(handler));
}
void emit(Args... args) {
for (auto& handler : handlers) {
handler(args...);
}
}
};
int main() {
Event<std::string, int> onMessage;
onMessage.subscribe([](const std::string& msg, int priority) {
std::cout << "[" << priority << "] " << msg << "\n";
});
onMessage.subscribe([](const std::string& msg, int priority) {
if (priority > 5) {
std::cout << "HIGH PRIORITY: " << msg << "\n";
}
});
onMessage.emit("Server started", 3);
onMessage.emit("Critical error!", 9);
}Çıktı:
[3] Server started
[9] Critical error!
HIGH PRIORITY: Critical error!Event<std::string, int> handler'ları string ve int argümanla çağrır. Event<> argümansız event olur. Parameter pack sayesinde herhangi bir argüman kombinasyonuyla çalışır.
Bu pattern GUI framework'lerinde, oyun motorlarında ve network kütüphanelerinde çok yaygındır. Event tipini Event<MousePosition>, Event<KeyCode, bool>, Event<> gibi dilediğin gibi tanımlayabilirsin.
Index Sequence ile Pack Erişimi
Bazen pack'teki belirli elemanlarla çalışman gerekir. std::index_sequence bunu sağlar:
#include <iostream>
#include <tuple>
#include <utility>
// Tuple elemanlarını yazdır — index sequence ile
template<typename Tuple, std::size_t... Is>
void printTupleImpl(const Tuple& t, std::index_sequence<Is...>) {
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}
template<typename... Args>
void printTuple(const std::tuple<Args...>& t) {
std::cout << "(";
printTupleImpl(t, std::index_sequence_for<Args...>{});
std::cout << ")\n";
}
int main() {
auto t1 = std::make_tuple(1, "hello", 3.14);
printTuple(t1); // (1, hello, 3.14)
auto t2 = std::make_tuple("name", 42, true, 2.5);
printTuple(t2); // (name, 42, 1, 2.5)
}std::index_sequence_for<Args...> argüman sayısına göre 0, 1, 2, ... indeks dizisi oluşturur. Bu teknik, tuple elemanlarına sırayla erişmek için standart bir pattern'dır.
Gerçek Dünya: Tip Güvenli Format String
Variadic template'lerin en etkileyici kullanımlarından biri — printf benzeri ama tip güvenli formatlama:
#include <iostream>
#include <sstream>
#include <string>
#include <stdexcept>
// Base case — format string'de {} kalmamış olmalı
std::string format(const std::string& fmt) {
// Kalan {} kontrolü
if (fmt.find("{}") != std::string::npos) {
throw std::runtime_error("Too few arguments for format string");
}
return fmt;
}
// Recursive case
template<typename T, typename... Rest>
std::string format(const std::string& fmt, T first, Rest... rest) {
auto pos = fmt.find("{}");
if (pos == std::string::npos) {
throw std::runtime_error("Too many arguments for format string");
}
std::ostringstream oss;
oss << first;
std::string result = fmt.substr(0, pos) + oss.str() + fmt.substr(pos + 2);
return format(result, rest...);
}
int main() {
std::cout << format("Hello, {}! You are {} years old.", "Ali", 25) << "\n";
// Hello, Ali! You are 25 years old.
std::cout << format("{} + {} = {}", 3, 4, 7) << "\n";
// 3 + 4 = 7
std::cout << format("Pi is approximately {}", 3.14159) << "\n";
// Pi is approximately 3.14159
}Bu basit bir implementasyon. C++20'de std::format tam olarak bu mantıkla (ve çok daha sofistike şekilde) çalışır:
// C++20
#include <format>
std::string msg = std::format("Hello, {}! Age: {}", "Ali", 25);Variadic Template Avantajları ve Ne Zaman Kullanılmalı
Kullanım Alanları
Factory fonksiyonları:
make_unique,make_shared,emplace_backLoglama ve formatlama: Tip güvenli print, format
Event sistemi: Farklı argüman tipli event handler'lar
Tuple ve variant: Farklı tipleri tek yapıda tut
Forwarding wrapper'lar: Argümanları başka fonksiyona ilet
Ne Zaman Kullanılmamalı
Basit overloading yeterliyse: 2-3 parametre varyasyonu varsa overload yaz
initializer_list yeterliyse: Tüm argümanlar aynı tipteyse
std::initializer_list<T>daha basitOkunabilirlik düşüyorsa: Variadic template kodu karmaşık olabilir — takım arkadaşların anlayabilmeli
Debugging zorlaşıyorsa: Variadic template hata mesajları uzun ve anlaşılması güç olabilir
// initializer_list daha basit — tüm argümanlar aynı tip
void printAll(std::initializer_list<int> values) {
for (int v : values) std::cout << v << " ";
}
printAll({1, 2, 3, 4, 5});
// Variadic template gerekli — farklı tipler
template<typename... Args>
void printAll(Args... args) {
((std::cout << args << " "), ...);
}
printAll(1, "hello", 3.14, true);💡 Kural: Tüm argümanlar aynı tipteyse
initializer_listkullan. Farklı tiplerdeyse variadic template.
Özet
Parameter pack (
typename... Args), sıfır veya daha fazla tip parametresi alan template mekanizmasıdır.Args... argsile fonksiyon parametrelerine dönüşür.`sizeof...(Args)` pack'teki eleman sayısını derleme zamanında verir.
Recursive unpacking (C++11), pack'ten bir eleman çıkarıp kalanını tekrar çağırarak çalışır — base case fonksiyonu gerektirir.
Fold expressions (C++17), recursive yaklaşımı tek satıra indirger:
(args + ...),(std::cout << args, ...)gibi formlarla pack'i expand eder.Perfect forwarding (
std::forward<Args>(args)...), argümanları kopyalamadan hedefe iletir —make_unique,emplace_backgibi fonksiyonların temelini oluşturur.Variadic template'ler tip güvenli, genişletilebilir ve sıfır çalışma zamanı maliyetli generic programlama sağlar — modern C++ kütüphanelerinin temel yapı taşlarından biridir.
AI Asistan
Sorularını yanıtlamaya hazır