← Kursa Dön
📄 Text · 20 min

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:

KeywordAnlamı
co_awaitBir işlemin tamamlanmasını bekle (suspend)
co_yieldBir değer üret ve duraklat
co_returnCoroutine'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ü

FonksiyonNe Zaman ÇağrılırTipik Dönüş
initial_suspend()Coroutine başladığındasuspend_always → lazy, suspend_never → eager
final_suspend()Coroutine bittiğindesuspend_always → handle geçerli kalır
get_return_object()Çağırana dönecek nesneyi oluştururGenerator nesnesi
yield_value(T)co_yield çağrıldığındasuspend_always → duraklat
return_void() / return_value(T)co_return çağrıldığındavoid
unhandled_exception()Exception yakalanmazsaGenelde 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:

  1. await_ready() çağır → true ise duraklatma, devam et

  2. false ise coroutine'i duraklat, await_suspend() çağır

  3. Resume 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:

  1. promise.return_value(expr) veya promise.return_void() çağrılır

  2. Yerel değişkenler ters sırada yok edilir

  3. 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:

ÖzellikThreadCoroutine
ZamanlamaOS (preemptive)Programcı (cooperative)
Context switchPahalı (~1-10μs)Çok ucuz (~ns)
BellekStack (1-8MB per thread)Frame (genelde < 1KB)
Sayı limitiBinler (OS limiti)Milyonlar (bellek limiti)
Data raceMümkün (mutex gerekir)Yok (tek thread, gönüllü geçiş)
KullanımCPU-bound paralellikI/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:

CompilerMinimum VersiyonFlag
GCC10+-std=c++20 -fcoroutines
Clang14+-std=c++20
MSVC19.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ği

C++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 constexpr olamaz

  • main() fonksiyonu coroutine olamaz

  • Constructor ve destructor coroutine olamaz

  • auto veya concept return type kullanamazsın (dönüş tipi açıkça belirtilmeli)

  • variadic arguments (C-style ...) kullanılamaz — ama variadic template'ler kullanılabilir

  • Coroutine 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::filter ile compose edilebilir

  • promise_type yazmana gerek yok

  • std::ranges::input_range modelini 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_return keyword'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ı.