← Kursa Dön
📄 Text · 20 min

Function Pointers ve Callbacks

C++'ta fonksiyonlar sadece çağrılmak için var değildir — aynı zamanda değer olarak da kullanılabilirler. Bir fonksiyonun adresini alabilir, bir değişkende saklayabilir, başka bir fonksiyona parametre olarak geçirebilirsin. Bu mekanizmaya function pointer (fonksiyon işaretçisi) diyoruz.

Function pointer'lar C'den miras kalan güçlü ama söz dizimi olarak biraz ürkütücü bir özellik. Bu derste bu söz dizimini adım adım çözeceğiz, callback pattern'ini öğreneceğiz ve modern C++'ın sunduğu std::function ve lambda alternatiflerini karşılaştıracağız.

Analoji: Function pointer'ları bir telefon rehberi gibi düşün. Rehberde kişinin adı (fonksiyon adı) ve telefon numarası (fonksiyonun bellek adresi) var. Numarayı bir kağıda yazıp (pointer'a atayıp) birine verebilirsin. O kişi kağıttaki numarayı arayarak (pointer üzerinden çağırarak) aynı kişiye ulaşır. Kağıdı istediğin kişiye verebilirsin — bu da callback pattern'inin temelidir.


Function Pointer Söz Dizimi

C++ dilindeki en kafa karıştırıcı söz dizimlerinden biri function pointer'lardır. Ama adım adım bakınca aslında mantıklı.

Temel Söz Dizimi

#include <iostream>

// Normal bir fonksiyon
int topla(int a, int b) {
    return a + b;
}

int cikar(int a, int b) {
    return a - b;
}

int main() {
    // Function pointer tanımlama
    // Dönüş_tipi (*pointer_adı)(parametre_tipleri)
    int (*islem)(int, int);
    
    // Fonksiyonun adresini ata
    islem = topla;      // & işareti opsiyonel: &topla da olur
    
    // Pointer üzerinden çağır
    int sonuc = islem(3, 4);  // topla(3, 4) = 7
    std::cout << "3 + 4 = " << sonuc << std::endl;
    
    // Farklı bir fonksiyona yönlendir
    islem = cikar;
    sonuc = islem(10, 3);  // cikar(10, 3) = 7
    std::cout << "10 - 3 = " << sonuc << std::endl;
    
    return 0;
}

Söz dizimini parçalayalım: int (*islem)(int, int)

  • int → dönüş tipi

  • (*islem)islem adında bir pointer (parantezler zorunlu!)

  • (int, int) → iki int parametre alan

Parantezler neden zorunlu? Çünkü int *islem(int, int) yazarsan, bu "int pointer döndüren bir fonksiyon" anlamına gelir — tamamen farklı bir şey!

Adım Adım Okuma Tekniği

Function pointer bildirimlerini okurken "saat yönünde spiral" (clockwise spiral) kuralını uygulayabilirsin, ama daha basit bir yol var:

  1. Pointer adını bul: (*islem)

  2. Sağına bak — parametreler: (int, int) → bu bir fonksiyon

  3. Soluna bak — dönüş tipi: int

  4. Sonuç: "islem, iki int alan ve int döndüren bir fonksiyona pointer"

// Örnekler:
void (*callback)(void);           // parametresiz, void döndüren
double (*hesapla)(double, double); // iki double alan, double döndüren
bool (*karsilastir)(const std::string&, const std::string&);
                                   // iki string ref alan, bool döndüren

typedef ve using ile Basitleştirme

Function pointer söz dizimi karmaşık. Bunu typedef veya (daha modern) using ile basitleştirebilirsin.

typedef Yöntemi (C tarzı)

#include <iostream>

// "IslemPtr" artık "iki int alan, int döndüren fonksiyon pointer'ı" demek
typedef int (*IslemPtr)(int, int);

int topla(int a, int b) { return a + b; }
int cikar(int a, int b) { return a - b; }
int carp(int a, int b)  { return a * b; }

// Şimdi parametre olarak kullanmak çok temiz
int hesapla(IslemPtr islem, int x, int y) {
    return islem(x, y);
}

int main() {
    std::cout << hesapla(topla, 5, 3) << std::endl;  // 8
    std::cout << hesapla(cikar, 5, 3) << std::endl;  // 2
    std::cout << hesapla(carp, 5, 3) << std::endl;   // 15
    return 0;
}

using Yöntemi (Modern C++ — Önerilen)

using söz dizimi daha okunabilir çünkü isim solda, tip sağda:

#include <iostream>

// using ile — çok daha okunabilir
using IslemPtr = int(*)(int, int);
using Comparator = bool(*)(int, int);
using Callback = void(*)(const std::string&);

int topla(int a, int b) { return a + b; }
int cikar(int a, int b) { return a - b; }

void islemYap(IslemPtr fn, int a, int b) {
    std::cout << "Sonuç: " << fn(a, b) << std::endl;
}

int main() {
    islemYap(topla, 10, 5);   // Sonuç: 15
    islemYap(cikar, 10, 5);   // Sonuç: 5
    return 0;
}

using IslemPtr = int(*)(int, int); satırını şöyle oku: "IslemPtr, iki int alıp int döndüren bir fonksiyon pointer'ıdır." Çok daha net, değil mi?


Callback Pattern

Callback (geri çağırma), bir fonksiyonun davranışını dışarıdan parametrik olarak değiştirme yöntemidir. En klasik örnek: bir sıralama fonksiyonuna karşılaştırma kuralını dışarıdan vermek.

Sıralama ile Callback

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

using Comparator = bool(*)(int, int);

// Genel amaçlı bubble sort — karşılaştırma mantığı dışarıdan gelir
void bubbleSort(std::vector<int>& arr, Comparator cmp) {
    int n = arr.size();
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - i - 1; j++) {
            if (cmp(arr[j], arr[j+1])) {
                std::swap(arr[j], arr[j+1]);
            }
        }
    }
}

// Farklı karşılaştırma fonksiyonları
bool kucuktenBuyuge(int a, int b) { return a > b; }
bool buyuktenKucuge(int a, int b) { return a < b; }
bool ciftlerOnce(int a, int b) { 
    if (a % 2 != b % 2) return a % 2 > b % 2;
    return a > b;
}

int main() {
    std::vector<int> v = {5, 2, 8, 1, 9, 3};
    
    bubbleSort(v, kucuktenBuyuge);
    std::cout << "Artan: ";
    for (int x : v) std::cout << x << " ";  // 1 2 3 5 8 9
    std::cout << std::endl;
    
    bubbleSort(v, buyuktenKucuge);
    std::cout << "Azalan: ";
    for (int x : v) std::cout << x << " ";  // 9 8 5 3 2 1
    std::cout << std::endl;
    
    bubbleSort(v, ciftlerOnce);
    std::cout << "Çiftler önce: ";
    for (int x : v) std::cout << x << " ";  // 2 8 1 3 5 9
    std::cout << std::endl;
    
    return 0;
}

Burada bubbleSort fonksiyonu sıralamanın "nasıl" yapılacağını bilir, ama "neye göre" sıralanacağını bilmez. Bu bilgiyi callback fonksiyonu sağlar. Bu, Strategy pattern'inin en basit halidir.

Olay Tabanlı Callback

GUI programlama, oyun geliştirme ve asenkron programlamada callback'ler her yerdedir:

#include <iostream>
#include <string>

using OnClick = void(*)(const std::string&);
using OnHover = void(*)(const std::string&);

struct Button {
    std::string label;
    OnClick onClick;
    OnHover onHover;
    
    void click() {
        if (onClick) onClick(label);
    }
    
    void hover() {
        if (onHover) onHover(label);
    }
};

void handleClick(const std::string& label) {
    std::cout << "[CLICK] " << label << " butonuna tıklandı!" << std::endl;
}

void handleHover(const std::string& label) {
    std::cout << "[HOVER] " << label << " üzerine gelindi." << std::endl;
}

void logClick(const std::string& label) {
    std::cout << "[LOG] Tıklama kaydedildi: " << label << std::endl;
}

int main() {
    Button btn1{"Kaydet", handleClick, handleHover};
    Button btn2{"Logla", logClick, nullptr};  // hover yok
    
    btn1.hover();   // [HOVER] Kaydet üzerine gelindi.
    btn1.click();   // [CLICK] Kaydet butonuna tıklandı!
    btn2.click();   // [LOG] Tıklama kaydedildi: Logla
    
    return 0;
}

⚠️ Dikkat: Function pointer nullptr olabilir. Callback'i çağırmadan önce mutlaka null kontrolü yap (if (onClick) onClick(label);). Aksi takdirde nullptr'ı çağırma girişimi undefined behavior oluşturur ve büyük olasılıkla crash'e yol açar.


Function Pointer Dizileri — Dispatch Table

Function pointer'ların en güçlü kullanımlarından biri dispatch table (dağıtım tablosu) oluşturmaktır. Bu, uzun if-else veya switch-case zincirlerinin yerine kullanılabilir.

Hesap Makinesi Dispatch Table

#include <iostream>

double topla(double a, double b) { return a + b; }
double cikar(double a, double b) { return a - b; }
double carp(double a, double b)  { return a * b; }
double bol(double a, double b)   { 
    if (b == 0.0) {
        std::cerr << "Sıfıra bölme hatası!" << std::endl;
        return 0.0;
    }
    return a / b; 
}

int main() {
    using IslemFn = double(*)(double, double);
    
    // Dispatch table — index ile fonksiyon seç
    IslemFn islemler[] = {topla, cikar, carp, bol};
    const char* isimler[] = {"+", "-", "*", "/"};
    
    double a = 10.0, b = 3.0;
    
    // switch-case yerine dispatch table!
    for (int i = 0; i < 4; i++) {
        double sonuc = islemler[i](a, b);
        std::cout << a << " " << isimler[i] << " " << b 
                  << " = " << sonuc << std::endl;
    }
    
    // Kullanıcıdan seçim al
    std::cout << "\nİşlem seçin (0:+, 1:-, 2:*, 3:/): ";
    int secim;
    std::cin >> secim;
    
    if (secim >= 0 && secim < 4) {
        std::cout << "Sonuç: " << islemler[secim](a, b) << std::endl;
    }
    
    return 0;
}

Bu yaklaşımın avantajları:

  • Yeni işlem eklemek için sadece diziye bir eleman eklersin.

  • Çalışma zamanında (runtime) hangi fonksiyonun çağrılacağını seçebilirsin.

  • switch-case zincirinden çok daha temiz ve genişletilebilir.

Command Pattern ile Dispatch Table

Daha gelişmiş bir örnek — bir metin editörünün komut sistemi:

#include <iostream>
#include <string>
#include <map>

using Command = void(*)();

void cmdNew()    { std::cout << "Yeni dosya oluşturuldu." << std::endl; }
void cmdOpen()   { std::cout << "Dosya açılıyor..." << std::endl; }
void cmdSave()   { std::cout << "Dosya kaydedildi." << std::endl; }
void cmdClose()  { std::cout << "Dosya kapatıldı." << std::endl; }
void cmdHelp()   {
    std::cout << "Komutlar: new, open, save, close, help, quit" << std::endl;
}

int main() {
    // String → fonksiyon eşlemesi
    std::map<std::string, Command> commands;
    commands["new"]   = cmdNew;
    commands["open"]  = cmdOpen;
    commands["save"]  = cmdSave;
    commands["close"] = cmdClose;
    commands["help"]  = cmdHelp;
    
    std::string input;
    std::cout << "Komut girin (quit ile çık): ";
    
    while (std::getline(std::cin, input)) {
        if (input == "quit") break;
        
        auto it = commands.find(input);
        if (it != commands.end()) {
            it->second();  // Fonksiyonu çağır
        } else {
            std::cout << "Bilinmeyen komut: " << input << std::endl;
        }
        
        std::cout << "Komut girin: ";
    }
    
    return 0;
}

std::map<std::string, Command> kullanarak string tabanlı bir dispatch table oluşturduk. Bu pattern, özellikle komut satırı uygulamaları, oyun scriptleri ve plugin sistemlerinde çok yaygındır.


Fonksiyonlara Fonksiyon Geçirme — Template Yaklaşımı

Function pointer'lar C tarzı bir çözüm. C++'ta template kullanarak daha genel ve daha performanslı bir yaklaşım da mümkün:

#include <iostream>
#include <vector>
#include <algorithm>

// Template ile — her türlü callable kabul eder
template<typename Func>
void herBirine(const std::vector<int>& v, Func f) {
    for (int x : v) {
        f(x);
    }
}

void ekranaYaz(int x) {
    std::cout << x << " ";
}

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    
    // Function pointer geç
    herBirine(v, ekranaYaz);
    std::cout << std::endl;
    
    // Lambda geç — aynı fonksiyon, farklı callable
    herBirine(v, [](int x) {
        std::cout << x * x << " ";
    });
    std::cout << std::endl;
    
    return 0;
}

Template yaklaşımının avantajı: derleyici tam olarak hangi fonksiyonun çağrılacağını bilir, bu yüzden inline edebilir. Function pointer ile bu mümkün değildir çünkü pointer'ın değeri runtime'da belli olur.


std::function — Esnek Alternatif

C++11 ile gelen std::function, function pointer'ların daha güçlü ve esnek halidir. <functional> başlık dosyasında tanımlıdır.

Nedir, Neden Var?

Function pointer'lar sadece sıradan fonksiyonlara (free functions) ve stateless lambda'lara (yakalama listesi boş olan) işaret edebilir. Ama gerçek dünyada şunlara da ihtiyaç duyarsın:

  • Capture'lı lambda'lar (dışarıdan değişken yakalayan)

  • Functor'lar (function call operator'ü olan nesneler)

  • Member fonksiyonlar

std::function tüm bunları tek bir tipte sarar:

#include <iostream>
#include <functional>
#include <string>

int topla(int a, int b) { return a + b; }

// Functor (fonksiyon nesnesi)
struct Carpici {
    int katsayi;
    int operator()(int x) const { return x * katsayi; }
};

int main() {
    // Sıradan fonksiyon
    std::function<int(int, int)> fn1 = topla;
    std::cout << fn1(3, 4) << std::endl;  // 7
    
    // Lambda (capture'lı)
    int offset = 100;
    std::function<int(int)> fn2 = [offset](int x) { return x + offset; };
    std::cout << fn2(42) << std::endl;  // 142
    
    // Functor
    Carpici c{5};
    std::function<int(int)> fn3 = c;
    std::cout << fn3(10) << std::endl;  // 50
    
    // Boş kontrol — function pointer gibi nullptr olabilir
    std::function<void()> fn4;
    if (!fn4) {
        std::cout << "fn4 boş!" << std::endl;
    }
    
    return 0;
}

Söz dizimi: std::function<DönüşTipi(Parametre1, Parametre2, ...)>

Bu, function pointer'ın söz diziminden çok daha okunabilir:

  • Function pointer: int (*fn)(int, int)

  • std::function: std::function<int(int, int)>

Event Handler Sistemi

std::function ile gerçek bir event handler sistemi kuralım:

#include <iostream>
#include <functional>
#include <vector>
#include <string>
#include <map>

class EventSystem {
    // Her event adı için callback listesi
    std::map<std::string, std::vector<std::function<void(const std::string&)>>> handlers_;
    
public:
    // Event'e listener ekle
    void on(const std::string& event, 
            std::function<void(const std::string&)> handler) {
        handlers_[event].push_back(handler);
    }
    
    // Event'i tetikle
    void emit(const std::string& event, const std::string& data = "") {
        auto it = handlers_.find(event);
        if (it != handlers_.end()) {
            for (auto& handler : it->second) {
                handler(data);
            }
        }
    }
};

int main() {
    EventSystem events;
    
    // Farklı türde callback'ler ekle
    // 1. Lambda
    events.on("click", [](const std::string& data) {
        std::cout << "Lambda handler: " << data << std::endl;
    });
    
    // 2. Capture'lı lambda
    int clickCount = 0;
    events.on("click", [&clickCount](const std::string& data) {
        clickCount++;
        std::cout << "Click #" << clickCount << ": " << data << std::endl;
    });
    
    // 3. Sıradan fonksiyon
    events.on("error", [](const std::string& msg) {
        std::cerr << "[ERROR] " << msg << std::endl;
    });
    
    // Event'leri tetikle
    events.emit("click", "Kaydet butonu");
    events.emit("click", "İptal butonu");
    events.emit("error", "Bağlantı zaman aşımı");
    
    return 0;
}

Bu sistem std::function olmadan yazılamazdı, çünkü capture'lı lambda'lar function pointer'a dönüştürülemez. Function pointer sadece state'siz çağrılabilir nesnelerle çalışır.


Lambda vs Function Pointer vs std::function

Bu üçünü ne zaman kullanmalısın? İşte kapsamlı bir karşılaştırma:

Karşılaştırma Tablosu

ÖzellikFunction Pointerstd::functionLambda (template ile)
Söz dizimiKarmaşıkTemizEn temiz
Capture❌ Yok✅ Var✅ Var
Inline olabilir mi?❌ Hayır*❌ Hayır✅ Evet
Heap allocation❌ Yok⚠️ Olabilir❌ Yok
Boyut8 byte (64-bit)~32-64 byteCapture'a göre değişir
nullptr olabilir mi?✅ Evet✅ Evet❌ Hayır
Runtime overheadMinimalVar (type erasure)Yok
C API uyumlu✅ Evet❌ Hayır❌ Hayır
Depolanabilir✅ Evet✅ Evet⚠️ Template gerekir
Functor destekler❌ Hayır✅ Evet✅ Evet

*\* Derleyici bazı özel durumlarda devirtualize edip inline edebilir, ama bu garantili değildir.*

Ne Zaman Hangisi?

Function pointer kullan:

  • C kütüphaneleriyle çalışıyorsan (C API callback'leri)

  • Capture'a ihtiyaç yoksa ve performans kritikse

  • Eski kodla (legacy code) uyumluluk gerekiyorsa

std::function kullan:

  • Callback'i bir sınıfın member'ında saklamanız gerekiyorsa

  • Capture'lı lambda, functor veya member function desteği lazımsa

  • Runtime'da farklı callable tipleri arasında geçiş yapılacaksa

  • Koleksiyon içinde (vector, map) callable saklanacaksa

Template + Lambda kullan:

  • Performans kritikse (inline + zero overhead)

  • Compile-time polimorfizm yeterliyse

  • STL algoritmaları gibi generic kodlarda

#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>

// 1. Function pointer — basit, C uyumlu
void cCallback(int value) {
    std::cout << "C-style: " << value << std::endl;
}

// 2. Template — en performanslı, inline olabilir
template<typename Func>
void applyTemplate(int value, Func f) {
    f(value);
}

// 3. std::function — en esnek, ama overhead var
void applyFunction(int value, std::function<void(int)> f) {
    f(value);
}

int main() {
    int multiplier = 3;
    
    // Function pointer — capture OLMAZ
    void (*fp)(int) = cCallback;
    fp(42);
    
    // Template — capture olur, inline olur
    applyTemplate(42, [multiplier](int x) {
        std::cout << "Template: " << x * multiplier << std::endl;
    });
    
    // std::function — capture olur, ama overhead var
    applyFunction(42, [multiplier](int x) {
        std::cout << "std::function: " << x * multiplier << std::endl;
    });
    
    return 0;
}

Performans Farkları

Performans konusu genellikle abartılır, ama yoğun döngülerde fark edilebilir.

Function Pointer vs Indirect Call

Function pointer dolaylı bir çağrıdır (indirect call). CPU, hangi fonksiyonun çağrılacağını runtime'da çözer. Bu, branch prediction miss'lerine ve instruction cache miss'lerine yol açabilir. Ama pratikte modern CPU'lar indirect call'ları çok iyi tahmin eder.

std::function Overhead'i

std::function type erasure kullanır. Bu da demek oluyor ki:

  1. Callable nesneyi sarmak için sanal fonksiyon benzeri bir mekanizma kullanır.

  2. Küçük callable'lar "small buffer optimization" (SBO) ile yerinde tutulur.

  3. Büyük callable'lar (çok capture'lı lambda'lar) heap'te ayrılır — new / delete overhead'i.

#include <functional>
#include <iostream>

int main() {
    // Boyut karşılaştırması
    int (*fp)(int) = nullptr;
    std::function<int(int)> fn;
    
    std::cout << "Function pointer boyutu: " << sizeof(fp) << " byte" << std::endl;
    std::cout << "std::function boyutu:    " << sizeof(fn) << " byte" << std::endl;
    
    // Tipik çıktı (64-bit sistemde):
    // Function pointer boyutu: 8 byte
    // std::function boyutu:    32 byte (veya 64, implementasyona göre)
    
    return 0;
}

💡 İpucu: Performans kaygısı varsa, önce ölç sonra optimize et. Çoğu uygulamada std::function'ın overhead'i ihmal edilebilir düzeydedir. Sadece çok sıkı döngülerde (milyon+ çağrı/saniye) fark hissedilebilir. O durumda bile template + lambda yaklaşımını tercih et, function pointer'a geri dönme.


Member Function Pointer'lar

Sınıf üye fonksiyonlarına da pointer alabilirsin, ama söz dizimi biraz daha karmaşık:

#include <iostream>
#include <string>

class Player {
public:
    std::string name;
    int health;
    
    Player(const std::string& n, int h) : name(n), health(h) {}
    
    void heal(int amount) {
        health += amount;
        std::cout << name << " iyileşti! Can: " << health << std::endl;
    }
    
    void damage(int amount) {
        health -= amount;
        std::cout << name << " hasar aldı! Can: " << health << std::endl;
    }
};

int main() {
    // Member function pointer söz dizimi
    // Dönüş_tipi (Sınıf::*pointer_adı)(parametreler)
    void (Player::*action)(int) = &Player::heal;
    
    Player p("Ahmet", 100);
    
    // Çağırma: nesne üzerinden .* operatörü
    (p.*action)(20);   // Ahmet iyileşti! Can: 120
    
    // Farklı fonksiyona yönlendir
    action = &Player::damage;
    (p.*action)(30);   // Ahmet hasar aldı! Can: 90
    
    // Pointer üzerinden ->* operatörü
    Player* ptr = &p;
    action = &Player::heal;
    (ptr->*action)(50);  // Ahmet iyileşti! Can: 140
    
    return 0;
}

Gördüğün gibi .* ve ->* operatörleri kullanılıyor. Bu söz dizimi gerçekten çirkin. Pratikte member function pointer'lar yerine std::function veya std::bind (veya lambda) kullanmak çok daha temiz:

#include <functional>
#include <iostream>

class Calculator {
public:
    double add(double a, double b) { return a + b; }
    double multiply(double a, double b) { return a * b; }
};

int main() {
    Calculator calc;
    
    // Lambda ile sarma — en temiz yol
    std::function<double(double, double)> op;
    
    op = [&calc](double a, double b) { return calc.add(a, b); };
    std::cout << op(3, 4) << std::endl;  // 7
    
    op = [&calc](double a, double b) { return calc.multiply(a, b); };
    std::cout << op(3, 4) << std::endl;  // 12
    
    return 0;
}

Pratik Örnek: Gelişmiş Hesap Makinesi

Öğrendiklerimizi birleştiren kapsamlı bir örnek yazalım:

#include <iostream>
#include <functional>
#include <map>
#include <string>
#include <cmath>
#include <stdexcept>

class Calculator {
    std::map<std::string, std::function<double(double, double)>> binaryOps_;
    std::map<std::string, std::function<double(double)>> unaryOps_;
    
public:
    Calculator() {
        // İkili işlemler
        binaryOps_["+"]   = [](double a, double b) { return a + b; };
        binaryOps_["-"]   = [](double a, double b) { return a - b; };
        binaryOps_["*"]   = [](double a, double b) { return a * b; };
        binaryOps_["/"]   = [](double a, double b) {
            if (b == 0) throw std::runtime_error("Sıfıra bölme!");
            return a / b;
        };
        binaryOps_["pow"] = [](double a, double b) { return std::pow(a, b); };
        
        // Tekli işlemler
        unaryOps_["sqrt"] = [](double x) { return std::sqrt(x); };
        unaryOps_["abs"]  = [](double x) { return std::abs(x); };
        unaryOps_["neg"]  = [](double x) { return -x; };
    }
    
    // Yeni ikili işlem ekle
    void addBinaryOp(const std::string& name, 
                     std::function<double(double, double)> op) {
        binaryOps_[name] = op;
    }
    
    // Yeni tekli işlem ekle
    void addUnaryOp(const std::string& name, 
                    std::function<double(double)> op) {
        unaryOps_[name] = op;
    }
    
    double compute(const std::string& op, double a, double b) {
        auto it = binaryOps_.find(op);
        if (it == binaryOps_.end()) {
            throw std::runtime_error("Bilinmeyen işlem: " + op);
        }
        return it->second(a, b);
    }
    
    double compute(const std::string& op, double x) {
        auto it = unaryOps_.find(op);
        if (it == unaryOps_.end()) {
            throw std::runtime_error("Bilinmeyen işlem: " + op);
        }
        return it->second(x);
    }
};

int main() {
    Calculator calc;
    
    // Yerleşik işlemleri kullan
    std::cout << "10 + 3 = " << calc.compute("+", 10, 3) << std::endl;
    std::cout << "2 ^ 10 = " << calc.compute("pow", 2, 10) << std::endl;
    std::cout << "sqrt(144) = " << calc.compute("sqrt", 144) << std::endl;
    
    // Yeni işlem ekle — genişletilebilirlik!
    calc.addBinaryOp("mod", [](double a, double b) {
        return std::fmod(a, b);
    });
    
    std::cout << "17 mod 5 = " << calc.compute("mod", 17, 5) << std::endl;
    
    return 0;
}

Bu tasarım Open-Closed Principle'ı (Açık-Kapalı prensibi) güzelce uygular: hesap makinesi sınıfını değiştirmeden yeni işlemler ekleyebilirsin.


std::function ile Dikkat Edilmesi Gerekenler

Boş std::function Çağrısı

#include <functional>

int main() {
    std::function<void()> fn;  // Boş
    
    // YANLIŞ — std::bad_function_call fırlatır!
    // fn();
    
    // DOĞRU — önce kontrol et
    if (fn) {
        fn();
    }
    return 0;
}

Yaşam Süresi (Lifetime) Sorunları

#include <functional>
#include <iostream>

std::function<int()> tehlikeliFn() {
    int x = 42;
    // TEHLIKE — x fonksiyon bitince yok olur!
    return [&x]() { return x; };  // Dangling reference!
}

std::function<int()> guvenilirFn() {
    int x = 42;
    // DOĞRU — x'i kopyala (capture by value)
    return [x]() { return x; };
}

int main() {
    auto bad = tehlikeliFn();
    // bad() çağırmak UB! x artık yok.
    
    auto good = guvenilirFn();
    std::cout << good() << std::endl;  // 42, güvenli
    
    return 0;
}

⚠️ Dikkat: std::function içine lambda saklarken capture moduna çok dikkat et. Referans ile yakalanan değişkenler, lambda'yı oluşturan scope sona erince geçersiz olur. Callback'lerde value capture ([=] veya [x]) genellikle daha güvenlidir.


C API Callback'leri ile Çalışmak

Bazen C kütüphaneleri (SDL, GLFW, SQLite vb.) callback olarak function pointer bekler. Bu durumda std::function veya capture'lı lambda kullanamazsın — düz function pointer vermelisin:

#include <iostream>

// C-tarzı API simülasyonu
extern "C" {
    typedef void (*EventCallback)(int eventCode, void* userData);
    
    void registerCallback(EventCallback cb, void* userData) {
        // Simülasyon: callback'i çağır
        cb(42, userData);
        cb(99, userData);
    }
}

// C callback'ine uyumlu fonksiyon
void myHandler(int eventCode, void* userData) {
    std::string* name = static_cast<std::string*>(userData);
    std::cout << *name << " — Event: " << eventCode << std::endl;
}

int main() {
    std::string appName = "MyApp";
    
    // void* userData ile state taşınır
    registerCallback(myHandler, &appName);
    
    return 0;
}

C API'lerinde state taşımak için void* userData parametresi kullanılır. Bu, type-safety'den yoksun ama C'nin sunduğu tek yoldur. Modern C++ kodunda bu pattern'e ihtiyaç duymazsın.


Özet

  • Function pointer bir fonksiyonun bellek adresini tutan değişkendir. Söz dizimi karmaşıktır: int (*fp)(int, int). using ile basitleştir: using FnPtr = int(*)(int, int);

  • Callback pattern, bir fonksiyonun davranışını dışarıdan özelleştirmek için function pointer/callable geçirme yöntemidir. Sıralama, event handling, strategy pattern gibi pek çok yerde kullanılır.

  • Dispatch table, function pointer dizisi veya map'i ile switch-case zincirlerinin yerine kullanılabilir. Daha temiz, genişletilebilir ve bakımı kolaydır.

  • std::function function pointer'ın modern, esnek alternatifidir. Lambda, functor, member function — her türlü callable'ı sarar. Ama type erasure nedeniyle hafif bir runtime overhead'i vardır.

  • Template + Lambda performans açısından en iyi seçenektir. Derleyici inline edebilir, zero overhead sunar. STL algoritmaları bu yaklaşımı kullanır.

  • Doğru aracı seç: C API uyumluluğu için function pointer, esneklik için std::function, performans için template + lambda. Çoğu günlük kodda std::function veya lambda ile template yeterlidir.