C++20 Coroutines
Bir restoran düşün. Geleneksel bir garson (fonksiyon) siparişi alır, mutfağa gider, yemeği bekler, getirir ve ancak o zaman bir sonraki masaya geçer. Yemek hazırlanırken garson orada dikilip bekler — hiçbir şey yapmaz. Akıllı bir garson (coroutine) ise siparişi mutfağa verir, yemek hazırlanırken başka masalara bakar, yemek hazır olunca geri gelip servis yapar. Aynı garson, aynı anda birden fazla masayı idare eder — çünkü bekleme anlarında duraklar ve başka iş yapar.
İşte C++20 coroutine'leri tam olarak bu. Normal fonksiyonlar çağrıldığında baştan sona çalışır ve biter. Coroutine'ler ise çalışırken duraklatılabilir (suspend) ve daha sonra kaldığı yerden devam edebilir (resume). Bu yetenek lazy evaluation, generator pattern'leri ve asenkron programlama gibi güçlü teknikleri mümkün kılıyor.
Bu derste coroutine'lerin mekanizmasını sıfırdan öğreneceğiz — co_await, co_yield, co_return keyword'lerini, promise_type kontratını, pratik generator örneklerini ve thread'lerle karşılaştırmayı ele alacağız.
Fonksiyon vs Coroutine
Normal Fonksiyonların Sınırı
Normal bir fonksiyon çağrıldığında tek bir giriş noktası ve tek bir çıkış noktası vardır. Fonksiyon başlar, işini yapar, return ile biter. Ortasında durup sonra devam etme gibi bir lüksü yoktur:
#include <iostream>
#include <vector>
// Normal fonksiyon: başlar, çalışır, biter
std::vector<int> ilkNSayi(int n) {
std::vector<int> sonuc;
for (int i = 0; i < n; ++i) {
sonuc.push_back(i);
}
return sonuc; // Tek çıkış noktası, tüm veri hazır olmalı
}
int main() {
// 1 milyon sayı istesek, hepsi bellekte oluşturulur
auto sayilar = ilkNSayi(1'000'000); // 4MB bellek bir anda!
// Belki sadece ilk 10'una bakacaktık...
for (int i = 0; i < 10; ++i) {
std::cout << sayilar[i] << " ";
}
}Bu fonksiyon 1 milyon sayı üretir ama belki sadece 10 tanesini kullanacaktık. Tüm veri önceden hesaplanır ve belleğe yazılır. Coroutine ile bu sorunu ortadan kaldırırız — her sayıyı lazım olduğunda, tek tek üretiriz.
Coroutine'in Farkı
Bir coroutine, birden fazla giriş ve çıkış noktasına sahip bir fonksiyondur. co_yield ile bir değer üretip duraklar, çağıran taraf istediğinde devam eder. Yani coroutine bir fonksiyon değil, bir durum makinesidir (state machine) — compiler tarafından otomatik olarak dönüştürülür.
C++20'de bir fonksiyonun coroutine olması için gövdesinde şu üç keyword'den en az birini kullanması gerekir:
| Keyword | Anlamı |
|---|---|
co_await | Bir işlemin tamamlanmasını bekle (suspend) |
co_yield | Bir değer üret ve duraklat |
co_return | Coroutine'i sonlandır |
Compiler bu keyword'lerden birini gördüğünde fonksiyonu normal bir fonksiyon olarak derlemez. Onu bir coroutine frame oluşturarak state machine'e dönüştürür.
Coroutine'in İç Yapısı: promise_type
Coroutine sistemi üç parçadan oluşur: coroutine frame (durum bilgisini tutan heap alanı), promise_type (coroutine'in davranışını tanımlayan kontrat) ve coroutine handle (coroutine'i kontrol eden pointer).
promise_type Nedir?
promise_type, coroutine'in "sözleşmesi"dir. Coroutine başladığında ne olacak? Duraklatıldığında ne olacak? Değer ürettiğinde ne olacak? Bittiğinde ne olacak? Tüm bu soruların cevabı promise_type içinde tanımlanır.
#include <coroutine>
#include <iostream>
#include <optional>
// Generator tipi: coroutine'in dönüş tipi
template <typename T>
struct Generator {
// promise_type: coroutine contract
struct promise_type {
T current_value;
// Coroutine başladığında duraklat mı?
std::suspend_always initial_suspend() { return {}; }
// Coroutine bittiğinde duraklat mı?
std::suspend_always final_suspend() noexcept { return {}; }
// Generator nesnesini oluştur
Generator get_return_object() {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
// co_yield çağrıldığında
std::suspend_always yield_value(T value) {
current_value = value;
return {}; // Duraklat
}
// co_return (void) çağrıldığında
void return_void() {}
// Exception fırlarsa
void unhandled_exception() { std::terminate(); }
};
// Coroutine handle
std::coroutine_handle<promise_type> handle_;
explicit Generator(std::coroutine_handle<promise_type> h) : handle_(h) {}
~Generator() {
if (handle_) handle_.destroy();
}
// Move only — kopyalama yasak
Generator(const Generator&) = delete;
Generator& operator=(const Generator&) = delete;
Generator(Generator&& other) noexcept : handle_(other.handle_) {
other.handle_ = nullptr;
}
// Iterator arayüzü
bool next() {
if (!handle_ || handle_.done()) return false;
handle_.resume();
return !handle_.done();
}
T value() const {
return handle_.promise().current_value;
}
};Bu yapı ilk bakışta karmaşık görünebilir ama mantığı basit: promise_type coroutine'e "nasıl davranacağını" söyler. Generator ise dış dünyaya "bu coroutine'den nasıl değer alacağını" sunar.
promise_type Fonksiyonlarının Rolü
| Fonksiyon | Ne Zaman Çağrılır | Tipik Dönüş |
|---|---|---|
initial_suspend() | Coroutine başladığında | suspend_always → lazy, suspend_never → eager |
final_suspend() | Coroutine bittiğinde | suspend_always → handle geçerli kalır |
get_return_object() | Çağırana dönecek nesneyi oluşturur | Generator nesnesi |
yield_value(T) | co_yield çağrıldığında | suspend_always → duraklat |
return_void() / return_value(T) | co_return çağrıldığında | void |
unhandled_exception() | Exception yakalanmazsa | Genelde terminate() |
initial_suspend özellikle önemli. suspend_always döndürürse coroutine lazy olur — ilk resume() çağrılana kadar hiçbir kod çalışmaz. suspend_never döndürürse ilk suspend point'e kadar hemen çalışır.
co_yield: Generator Pattern
Basit Sayı Üretici
Artık Generator yapımız hazır. Şimdi coroutine yazalım:
#include <coroutine>
#include <iostream>
// ... (Yukarıdaki Generator tanımı dahil edilmiş varsayalım)
Generator<int> sayiUret(int baslangic, int bitis) {
for (int i = baslangic; i <= bitis; ++i) {
co_yield i; // Değer üret, duraklat
}
// Döngü bitince coroutine sona erer
}
int main() {
auto gen = sayiUret(1, 1'000'000);
// Sadece ilk 5 değeri al — geri kalanı asla hesaplanmaz!
for (int i = 0; i < 5 && gen.next(); ++i) {
std::cout << gen.value() << " ";
}
// Çıktı: 1 2 3 4 5
// Generator yok edildiğinde coroutine frame de temizlenir
}Burada sayiUret fonksiyonu 1'den 1 milyona kadar sayı üretebilir ama biz sadece 5 tane istediğimiz için sadece 5 iterasyon çalışır. Bu lazy evaluation'ın gücüdür — hesaplama sadece ihtiyaç duyulduğunda yapılır.
Fibonacci Generator
Klasik bir örnek: sonsuz Fibonacci dizisi. Normal fonksiyonla sonsuz dizi üretemezsin ama coroutine ile mümkün:
Generator<long long> fibonacci() {
long long a = 0, b = 1;
while (true) { // Sonsuz döngü! Ama sorun değil.
co_yield a;
long long temp = a + b;
a = b;
b = temp;
}
// Bu noktaya asla ulaşılmaz — sorun değil
}
int main() {
auto fib = fibonacci();
std::cout << "Ilk 15 Fibonacci sayisi:" << std::endl;
for (int i = 0; i < 15 && fib.next(); ++i) {
std::cout << fib.value() << " ";
}
// Çıktı: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377
}while (true) bir fonksiyonda felaket olur ama coroutine'de gayet güvenli. Her co_yield noktasında coroutine duraklar ve kontrolü çağırana verir. Çağıran next() dediğinde devam eder. Çağıran istemezse, coroutine yok edilir ve döngü doğal olarak sona erer.
Filtrelenmiş Generator
Generator'ları compose edebilirsin — birinin çıktısını diğerinin girdisi yapabilirsin:
Generator<int> ciftSayilar(Generator<int>& kaynak) {
while (kaynak.next()) {
int val = kaynak.value();
if (val % 2 == 0) {
co_yield val;
}
}
}
Generator<int> ilkN(Generator<int>& kaynak, int n) {
int sayac = 0;
while (sayac < n && kaynak.next()) {
co_yield kaynak.value();
++sayac;
}
}
int main() {
auto sayilar = sayiUret(1, 100);
auto ciftler = ciftSayilar(sayilar);
auto ilkBes = ilkN(ciftler, 5);
while (ilkBes.next()) {
std::cout << ilkBes.value() << " ";
}
// Çıktı: 2 4 6 8 10
// Sadece 10'a kadar olan sayılar değerlendirildi!
}Bu yaklaşım Unix pipe'larına benzer: sayiUret | ciftSayilar | ilkN. Her aşama sadece lazım olan kadar veri işler.
co_await: Asenkron Bekleme
Suspend Point Kavramı
co_await bir coroutine'in "burada dur, sonuç gelince devam et" demesidir. Bu, I/O işlemleri, ağ çağrıları veya başka coroutine'lerin sonuçları için kullanılır.
co_await bir awaitable nesne üzerinde çalışır. Awaitable nesne üç fonksiyon sağlamalıdır:
struct MyAwaitable {
// Duraklatmaya gerek var mı?
bool await_ready() { return false; } // false → duraklat
// Duraklatılırken ne yapılsın?
void await_suspend(std::coroutine_handle<> h) {
// Asenkron işlemi başlat, bitince h.resume() çağır
}
// Devam ettiğinde dönüş değeri ne olsun?
int await_resume() { return 42; }
};Compiler co_await expr gördüğünde şu adımları uygular:
await_ready()çağır →trueise duraklatma, devam etfalseise coroutine'i duraklat,await_suspend()çağırResume edildiğinde
await_resume()dönüş değerini al
Standart Awaitable'lar
C++20 iki hazır awaitable sağlar:
// Her zaman duraklat
struct std::suspend_always {
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<>) noexcept {}
void await_resume() noexcept {}
};
// Asla duraklatma
struct std::suspend_never {
bool await_ready() noexcept { return true; }
void await_suspend(std::coroutine_handle<>) noexcept {}
void await_resume() noexcept {}
};Bunlar promise_type'ta initial_suspend() ve final_suspend() dönüş değerleri olarak kullanılır. suspend_always "başlangıçta/sonda duraklat" demek, suspend_never "duraklatma" demek.
Basit Asenkron Simülasyon
Gerçek dünyada co_await genellikle I/O framework'leriyle (Boost.Asio, cppcoro gibi) birlikte kullanılır. Ama kavramı anlamak için basit bir simülasyon yazalım:
#include <coroutine>
#include <iostream>
#include <functional>
#include <queue>
// Basit Task tipi
struct Task {
struct promise_type {
Task get_return_object() {
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_never initial_suspend() { return {}; } // Hemen başla
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle_;
explicit Task(std::coroutine_handle<promise_type> h) : handle_(h) {}
~Task() { if (handle_) handle_.destroy(); }
Task(Task&& o) noexcept : handle_(o.handle_) { o.handle_ = nullptr; }
Task(const Task&) = delete;
};
// Global iş kuyruğu — basit scheduler
std::queue<std::coroutine_handle<>> isKuyrugu;
// Custom awaitable: kuyruğa ekle ve duraklat
struct KuyrugaEkle {
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
isKuyrugu.push(h); // Kuyruğa ekle, sonra devam ettirilecek
}
void await_resume() {}
};
Task gorevA() {
std::cout << "Gorev A: Basladi" << std::endl;
co_await KuyrugaEkle{}; // Duraklat, sırayı ver
std::cout << "Gorev A: Devam etti" << std::endl;
co_await KuyrugaEkle{};
std::cout << "Gorev A: Bitti" << std::endl;
}
Task gorevB() {
std::cout << "Gorev B: Basladi" << std::endl;
co_await KuyrugaEkle{};
std::cout << "Gorev B: Devam etti" << std::endl;
co_await KuyrugaEkle{};
std::cout << "Gorev B: Bitti" << std::endl;
}
int main() {
auto a = gorevA(); // initial_suspend = never, hemen "Basladi" yazar
auto b = gorevB();
// Kuyruktan çalıştır (basit round-robin scheduler)
while (!isKuyrugu.empty()) {
auto h = isKuyrugu.front();
isKuyrugu.pop();
h.resume();
}
}Çıktı:
Gorev A: Basladi
Gorev B: Basladi
Gorev A: Devam etti
Gorev B: Devam etti
Gorev A: Bitti
Gorev B: Bittiİki görev tek thread üzerinde sırayla çalıştı. Thread oluşturma maliyeti yok, mutex yok, race condition yok. co_await noktalarında gönüllü olarak duraklıyorlar ve scheduler sırayla devam ettiriyor.
co_return: Coroutine Sonlandırma
co_return bir coroutine'i sonlandırır. İki formu var:
// Değer döndüren coroutine
Task<int> hesapla() {
int sonuc = 42;
co_return sonuc; // promise.return_value(sonuc) çağırır
}
// Void coroutine
Task<void> isYap() {
// ... işlem ...
co_return; // promise.return_void() çağırır
}co_return çağrıldığında sırasıyla şunlar olur:
promise.return_value(expr)veyapromise.return_void()çağrılırYerel değişkenler ters sırada yok edilir
promise.final_suspend()çağrılır
final_suspend() önemli bir karar noktasıdır. suspend_always döndürürse coroutine frame bellekte kalır ve handle hâlâ geçerlidir — sonucu daha sonra okuyabilirsin. suspend_never döndürürse frame hemen yok edilir ve handle geçersiz hale gelir.
⚠️ Dikkat: co_return'den sonra coroutine handle'ı kullanıyorsan final_suspend() mutlaka suspend_always döndürmelidir. Aksi halde dangling handle (sarkan pointer gibi) oluşur ve undefined behavior kaçınılmazdır.
Task/Future Pattern
Generator'lar değer dizisi üretir. Ama bazen tek bir asenkron sonuç istiyoruz — "bu işlemi yap, bitince sonucu ver." Bu Task veya Future pattern'idir.
#include <coroutine>
#include <iostream>
#include <optional>
template <typename T>
struct Task {
struct promise_type {
std::optional<T> result;
std::coroutine_handle<> bekleyen = nullptr;
Task get_return_object() {
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_never initial_suspend() { return {}; }
auto final_suspend() noexcept {
// Bizi bekleyen varsa, onu devam ettir
struct Notifier {
std::coroutine_handle<> bekleyen;
bool await_ready() noexcept { return false; }
std::coroutine_handle<> await_suspend(
std::coroutine_handle<>) noexcept
{
return bekleyen ? bekleyen : std::noop_coroutine();
}
void await_resume() noexcept {}
};
return Notifier{bekleyen};
}
void return_value(T val) { result = std::move(val); }
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle_;
explicit Task(std::coroutine_handle<promise_type> h) : handle_(h) {}
~Task() { if (handle_) handle_.destroy(); }
Task(Task&& o) noexcept : handle_(o.handle_) { o.handle_ = nullptr; }
Task(const Task&) = delete;
// co_await desteği — bu Task'ı başka coroutine'de bekleyebilirsin
bool await_ready() {
return handle_.done();
}
void await_suspend(std::coroutine_handle<> h) {
handle_.promise().bekleyen = h;
}
T await_resume() {
return *handle_.promise().result;
}
T get() {
return *handle_.promise().result;
}
};
// Kullanım
Task<int> topla(int a, int b) {
co_return a + b;
}
Task<int> hesapla() {
int x = co_await topla(3, 4); // 7
int y = co_await topla(x, 10); // 17
co_return x + y; // 24
}
int main() {
auto sonuc = hesapla();
std::cout << "Sonuc: " << sonuc.get() << std::endl;
// Çıktı: Sonuc: 24
}Bu pattern'de Task<T> hem bir coroutine dönüş tipi hem de bir awaitable'dır. Bir coroutine başka bir coroutine'i co_await ile bekleyebilir. final_suspend'deki Notifier yapısı, iç Task bittiğinde bekleyen dış Task'ı otomatik olarak devam ettirir — symmetric transfer denen bu mekanizma stack overflow'u önler.
Coroutine vs Thread
İkisi de eşzamanlı (concurrent) çalışma sağlar ama temelden farklı mekanizmalardır:
| Özellik | Thread | Coroutine |
|---|---|---|
| Zamanlama | OS (preemptive) | Programcı (cooperative) |
| Context switch | Pahalı (~1-10μs) | Çok ucuz (~ns) |
| Bellek | Stack (1-8MB per thread) | Frame (genelde < 1KB) |
| Sayı limiti | Binler (OS limiti) | Milyonlar (bellek limiti) |
| Data race | Mümkün (mutex gerekir) | Yok (tek thread, gönüllü geçiş) |
| Kullanım | CPU-bound paralellik | I/O-bound concurrency |
// Thread yaklaşımı: 10.000 bağlantı = 10.000 thread = 10GB+ RAM
void threadYaklasimi() {
std::vector<std::thread> threadler;
for (int i = 0; i < 10'000; ++i) {
threadler.emplace_back([i]() {
// Her thread 1MB+ stack kullanır
// OS context switch maliyeti yüksek
baglantiIsle(i);
});
}
for (auto& t : threadler) t.join();
}
// Coroutine yaklaşımı: 10.000 bağlantı = 10.000 coroutine frame = ~10MB RAM
Task<void> coroutineYaklasimi() {
for (int i = 0; i < 10'000; ++i) {
co_await baglantiIsleAsync(i); // Tek thread, cooperative
}
}Thread'ler CPU-bound iş için (paralel hesaplama) idealdir. Coroutine'ler I/O-bound iş için (ağ sunucusu, dosya işlemleri) mükemmeldir. Pratikte ikisi birlikte kullanılır: bir thread pool üzerinde binlerce coroutine çalıştırılır.
💡 İpucu: Coroutine'ler thread'lerin yerine geçmez, tamamlayıcısıdır. Yüksek performanslı sunucularda tipik mimari şudur: N adet thread (CPU çekirdeği kadar) + her thread üzerinde binlerce coroutine. Bu yaklaşım thread-per-connection modelinden 10-100x daha verimlidir.
Compiler Desteği ve Kısıtlamalar
Compiler Desteği
C++20 coroutine'leri tüm major compiler'larda desteklenir:
| Compiler | Minimum Versiyon | Flag |
|---|---|---|
| GCC | 10+ | -std=c++20 -fcoroutines |
| Clang | 14+ | -std=c++20 |
| MSVC | 19.28+ (VS 2019 16.8) | /std:c++20 |
GCC 10-12 arasında -fcoroutines flag'i ayrıca gereklidir. GCC 13+ ile sadece -std=c++20 yeterlidir.
Kısıtlamalar
C++20 coroutine'leri altyapı seviyesinde tanımlanmıştır. Standart kütüphane hazır kullanılabilir coroutine tipleri sunmaz — Generator, Task gibi tipleri ya kendin yazarsın ya da bir kütüphane kullanırsın:
// C++20 standart kütüphanesinde BUNLAR YOK:
// std::generator → C++23'te geldi
// std::task → Henüz standart değil
// std::lazy → Henüz standart değil
// Kullanılabilir kütüphaneler:
// - cppcoro (Lewis Baker) — en olgun
// - folly::coro (Facebook)
// - Boost.Asio coroutine desteğiC++23 ile std::generator standarta girdi. Ancak Task, Future gibi tipler hâlâ kütüphanelere bırakılmış durumda.
Diğer kısıtlamalar:
Coroutine
constexprolamazmain()fonksiyonu coroutine olamazConstructor ve destructor coroutine olamaz
autoveya concept return type kullanamazsın (dönüş tipi açıkça belirtilmeli)variadic arguments(C-style...) kullanılamaz — ama variadic template'ler kullanılabilirCoroutine frame heap'te allocate edilir (compiler bazen bunu optimize edebilir — HALO: Heap Allocation eLision Optimization)
C++23: std::generator
C++23 ile birlikte nihayet standart kütüphanede bir generator tipi var. Artık yukarıdaki Generator template'ini elle yazmana gerek kalmıyor:
#include <generator> // C++23
#include <iostream>
#include <ranges>
std::generator<int> fibonacci() {
long long a = 0, b = 1;
while (true) {
co_yield a;
auto temp = a + b;
a = b;
b = temp;
}
}
std::generator<int> ciftler(std::generator<int> gen) {
for (auto val : gen) {
if (val % 2 == 0) {
co_yield val;
}
}
}
int main() {
// std::generator range-based for ile çalışır!
for (auto val : fibonacci() | std::views::take(10)) {
std::cout << val << " ";
}
// Çıktı: 0 1 1 2 3 5 8 13 21 34
}std::generator şu güzellikleri sağlar:
Range kavramıyla uyumlu →
views::take,views::filterile compose edilebilirpromise_typeyazmana gerek yokstd::ranges::input_rangemodelini karşılar
⚠️ Dikkat: std::generator C++23 özelliğidir. GCC 14+, Clang 18+ ve MSVC 17.10+ ile kullanılabilir. Projeniz C++20 ile sınırlıysa, kendi Generator template'inizi yazmanız veya cppcoro gibi kütüphaneleri kullanmanız gerekir.
Pratik: Lazy Evaluation ve Asenkron I/O Temsili
Lazy Dosya Satır Okuyucu
Büyük bir dosyayı satır satır okumak istediğinde tüm dosyayı belleğe almak yerine coroutine ile lazy okuma yapabilirsin:
#include <coroutine>
#include <fstream>
#include <iostream>
#include <string>
// Generator tanımı (önceki bölümdeki gibi)
// ...
Generator<std::string> satirOku(const std::string& dosyaAdi) {
std::ifstream dosya(dosyaAdi);
if (!dosya.is_open()) {
co_return; // Dosya açılamazsa sessizce bitir
}
std::string satir;
while (std::getline(dosya, satir)) {
co_yield satir; // Her satırı lazım olduğunda üret
}
// Dosya ifstream destructor ile otomatik kapanır (RAII)
}
int main() {
auto okuyucu = satirOku("buyuk_log.txt");
int sayac = 0;
while (okuyucu.next() && sayac < 100) {
const auto& satir = okuyucu.value();
if (satir.find("ERROR") != std::string::npos) {
std::cout << "Hata bulundu: " << satir << std::endl;
}
++sayac;
}
// 10GB'lık dosyada bile sadece 100 satır okundu
}Bu yaklaşımın güzelliği, 10GB'lık bir log dosyasında bile bellekte sadece tek bir satır tutulmasıdır. İhtiyacın kadarını oku, gerisini okuma.
Pipeline Pattern: Veri İşleme Hattı
Birden fazla generator'ı birleştirerek Unix pipe benzeri bir veri işleme hattı oluşturabilirsin:
// 1. Kaynak: Sayı üretici
Generator<int> sayiKaynagi(int n) {
for (int i = 1; i <= n; ++i) {
co_yield i;
}
}
// 2. Dönüşüm: Kare al
Generator<int> kareAl(Generator<int>& kaynak) {
while (kaynak.next()) {
int val = kaynak.value();
co_yield val * val;
}
}
// 3. Filtre: Belirli eşiğin altındakileri al
Generator<int> esikFiltre(Generator<int>& kaynak, int esik) {
while (kaynak.next()) {
int val = kaynak.value();
if (val < esik) {
co_yield val;
}
}
}
// 4. Tüketici
int main() {
auto sayilar = sayiKaynagi(100);
auto kareler = kareAl(sayilar);
auto filtreli = esikFiltre(kareler, 50);
// Pipeline: 1..100 → kare → <50 filtre
// Sadece gerekli sayılar hesaplanır!
while (filtreli.next()) {
std::cout << filtreli.value() << " ";
}
// Çıktı: 1 4 9 16 25 36 49
// Not: 8^2 = 64 >= 50, o noktadan sonra hepsi >= 50
// Ama tüm 100 sayı kareye alınmaz — lazım olan kadarı
}Her aşama sadece bir sonraki aşamanın talep ettiği kadar veri üretir. Bu, büyük veri setlerinde bellek ve CPU tasarrufu sağlar.
Yaygın Hatalar ve Tuzaklar
1. Dangling Reference
Coroutine'ler lifetime konusunda tehlikelidir. Coroutine frame heap'te yaşar ama yerel referanslar stack'te:
// YANLIS: reference parametre tehlikeli!
Generator<char> harfler(const std::string& str) {
for (char c : str) {
co_yield c; // str hâlâ geçerli mi?
}
}
Generator<char> tehlikeli() {
std::string gecici = "merhaba";
return harfler(gecici); // gecici burada yok edilir!
// Generator devam ettiğinde gecici artık geçersiz → UB!
}
// DOGRU: value olarak al veya coroutine'in içinde tut
Generator<char> harflerGuvenli(std::string str) { // Kopyala!
for (char c : str) {
co_yield c;
}
}Coroutine fonksiyonlarında parametre olarak referans almak risklidir çünkü coroutine, çağıran scope'tan daha uzun yaşayabilir. Parametreleri değer olarak (by value) almak en güvenli yoldur.
2. Coroutine Handle Leak
Coroutine frame heap'te allocate edilir ve açıkça destroy() edilmelidir. Handle kapsam dışına çıkar ve destroy() çağrılmazsa bellek sızıntısı olur:
// YANLIS: handle leak
void tehlikeli() {
auto gen = fibonacci();
gen.next();
// gen yok ediliyor ama eğer destructor handle_.destroy()
// çağırmıyorsa → LEAK!
}
// DOGRU: RAII ile garanti
// Generator destructor'ında mutlaka:
~Generator() {
if (handle_) handle_.destroy();
}3. return_void vs return_value Karışıklığı
promise_type'ta ya return_void() ya return_value(T) tanımlanmalıdır, ikisi birden olmaz:
// YANLIS: ikisi birden
struct promise_type {
void return_void() {}
void return_value(int) {} // Derleme hatası!
};
// Coroutine'de co_return kullanılmıyorsa → return_void
// Coroutine'de co_return expr kullanılıyorsa → return_valueÖzet
Coroutine, duraklatılıp devam ettirilebilen bir fonksiyondur.
co_await,co_yield,co_returnkeyword'lerinden birini kullanması yeterlidir.`co_yield` ile generator pattern uygulanır — sonsuz diziler bile lazy olarak, ihtiyaç duyulduğunda tek tek üretilir.
`co_await` ile asenkron bekleme yapılır — coroutine duraklar, sonuç hazır olduğunda kaldığı yerden devam eder.
`promise_type` coroutine'in davranış sözleşmesidir — başlangıçta duraklat mı, bitişte ne yap, değer üretilince ne olsun gibi soruları tanımlar.
Coroutine'ler thread'lerden çok daha hafiftir — tek thread üzerinde milyonlarca coroutine çalışabilir, context switch maliyeti neredeyse sıfırdır.
C++23 `std::generator` ile artık elle Generator yazmaya gerek kalmadı, ancak Task/Future gibi tipler hâlâ kütüphanelere veya kendi kodunuza bağlı.
AI Asistan
Sorularını yanıtlamaya hazır