← Kursa Dön
📄 Text · 18 min

Assert, static_assert ve Basit Test Stratejileri

Bir köprü inşa ettiğini düşün. Köprünün her aşamasında mühendisler kontrol noktaları koyar: "Bu kiriş en az 500 ton taşımalı", "Bu halat kopma gerilimi şu değerin altına düşmemeli." Eğer bir kontrol noktası başarısız olursa, inşaat hemen durur — çünkü devam etmek felakete davetiye çıkarmak demektir. İşte assert tam olarak bu: kodundaki kontrol noktası. "Bu koşul MUTLAKA doğru olmalı, yoksa bir şey çok yanlış gidiyor" demenin yolu.

Bu derste üç katmanlı bir savunma hattı kuracağız. Önce çalışma zamanında patlatılan assert() makrosunu öğreneceğiz. Sonra derleme zamanında hata veren static_assert ile tanışacağız. Son olarak, kodunu nasıl sistematik şekilde test edebileceğinin temellerini atacağız.


assert() Makrosu

Nedir ve Nereden Gelir?

assert(), C'den miras kalan bir makrodur (macro). C++'ta kullanmak için <cassert> header'ını dahil etmen yeterli. İşi çok basit: verdiğin ifade (expression) true ise hiçbir şey yapmaz, false ise programı anında durdurur ve sana hatanın nerede olduğunu söyler.

#include <cassert>
#include <iostream>

int bolme(int pay, int payda) {
    assert(payda != 0);  // payda sıfırsa program DURUR
    return pay / payda;
}

int main() {
    std::cout << bolme(10, 2) << std::endl;  // 5, sorunsuz
    std::cout << bolme(10, 0) << std::endl;  // BOOM! assert patlar
    return 0;
}

İkinci çağrıda program şuna benzer bir mesajla çöker:

Assertion failed: (payda != 0), function bolme, file main.cpp, line 5.
Abort

Bu mesaj sana üç kritik bilgi veriyor: hangi koşul başarısız oldu, hangi fonksiyonda ve hangi dosyanın kaçıncı satırında. Hata avında (debugging) bu altın değerinde.

Ne Zaman Kullanılır?

assert ile if arasındaki farkı anlamak çok önemli. İkisi de bir koşulu kontrol eder, ama felsefeleri tamamen farklıdır.

assert = Programcı hatası. "Bu durum asla olmamalı. Eğer oluyorsa, kodumda bir bug var." Mesela bir fonksiyona null pointer gelmemesi gerekiyorsa, bu bir programcı hatasıdır.

if = Beklenen durum. "Kullanıcı yanlış bir şey girebilir, dosya bulunamayabilir, ağ bağlantısı kopabilir." Bunlar normal, beklenen senaryolar. Bunları if ile ele alırsın.

#include <cassert>
#include <iostream>
#include <string>

// assert kullanımı: programcı hatası kontrolü
void diziyeElemanEkle(int* dizi, int boyut, int index, int deger) {
    assert(dizi != nullptr);     // dizi null olmamalı — programcı hatası
    assert(index >= 0);          // negatif index — programcı hatası  
    assert(index < boyut);       // sınır aşımı — programcı hatası
    dizi[index] = deger;
}

// if kullanımı: kullanıcı hatası kontrolü
int kullanicidanYasAl() {
    int yas;
    std::cout << "Yasinizi girin: ";
    std::cin >> yas;
    
    if (yas < 0 || yas > 150) {     // kullanıcı saçma bir şey girebilir
        std::cout << "Gecersiz yas!" << std::endl;
        return -1;
    }
    return yas;
}

Kural basit: Eğer koşulun başarısız olması "bu asla olmamalıydı" dedirten bir şeyse → assert. Eğer "olabilir, ele almam lazım" dedirten bir şeyse → if.

assert'in Anatomisi

assert() bir fonksiyon değil, bir makrodur (preprocessor macro). Bu ayrım önemli çünkü makrolar derleme öncesinde işlenir. Kabaca şöyle çalışır:

// assert kabaca bu şekilde tanımlanmıştır (basitleştirilmiş)
#ifdef NDEBUG
    #define assert(expr) ((void)0)   // hiçbir şey yapma
#else
    #define assert(expr) \
        ((expr) ? (void)0 : __assert_fail(#expr, __FILE__, __LINE__))
#endif

#expr kısmı, yazdığın ifadeyi string'e çevirir. Bu yüzden hata mesajında payda != 0 gibi okunabilir bir metin görürsün. __FILE__ ve __LINE__ ise derleyicinin otomatik doldurduğu dosya adı ve satır numarasıdır.


NDEBUG ve Release Build

Assert'ler Neden Kaybolur?

Yukarıdaki koda dikkat ettiysen, NDEBUG tanımlıyken assert'in tamamen yok olduğunu görmüşsündür. Bu tasarım gereği böyledir. Debug (geliştirme) modunda assert'ler aktiftir, ama release (yayın) modunda performans için devre dışı kalır.

Çoğu IDE ve build sistemi, release modda otomatik olarak NDEBUG tanımlar:

# Debug build — assert'ler aktif
g++ -g -o program main.cpp

# Release build — assert'ler devre dışı
g++ -O2 -DNDEBUG -o program main.cpp

Bu mantıklı: geliştirme sırasında hataları erken yakala, ama müşteriye verdiğin programda gereksiz kontrol yapma.

Yan Etki (Side Effect) Tuzağı

Bu, yeni başlayanların en sık düştüğü tuzak. Assert'in içinde programın davranışını değiştiren bir şey yapma. Çünkü release modda assert'in içindeki her şey buharlaşır.

⚠️ Dikkat: Assert ifadesinin içine asla yan etki (side effect) koyan kod yazma. Release build'de assert tamamen kaldırılır ve yan etki de birlikte kaybolur. Programın debug'da çalışıp release'de çökmesine neden olur — en zor bulunan bug türlerinden biri.

#include <cassert>
#include <vector>

int main() {
    std::vector<int> vec;
    
    // YANLIS! push_back bir yan etkidir
    // Release modda bu satır tamamen silinir, eleman eklenmez!
    assert(vec.empty());           // OK — sadece kontrol
    vec.push_back(42);
    assert(vec.size() == 1);       // OK — sadece kontrol
    
    // TEHLIKELI ORNEK (yapma!):
    // assert(vec.push_back(5), "push failed");  // Derlenmez bile ama niyet kötü
    
    // Daha sinsi versiyon:
    // assert(dosyaOku() == true);  // Release'de dosya OKUNMAZ!

    return 0;
}

Doğru yaklaşım: yan etkiyi assert'in dışında yap, sonucu bir değişkene ata, assert'te değişkeni kontrol et.

// DOGRU yaklaşım
bool sonuc = dosyaOku();   // yan etki burada, her zaman çalışır
assert(sonuc == true);      // kontrol burada, sadece debug'da çalışır

Bu alışkanlığı edinirsen, debug-release farklılıklarından kaynaklanan gizemli buglardan korunmuş olursun.


static_assert (C++11 ve Sonrası)

Derleme Zamanı Kontrolü

assert() çalışma zamanında (runtime) patlar. Ama bazı şeyleri daha erken, derleme zamanında (compile time) kontrol edebilirsek neden bekleme zamanı harcayalım? İşte static_assert tam bunu yapar.

Derleme zamanında kontrol edilebilen bir ifade verirsin. Eğer false ise program derlenmez bile ve sana anlamlı bir hata mesajı gösterir.

#include <iostream>
#include <cstdint>

// Platform varsayımlarını kontrol et
static_assert(sizeof(int) >= 4, "int en az 4 byte olmali");
static_assert(sizeof(void*) == 8, "64-bit platform gerekli");
static_assert(sizeof(char) == 1, "char her zaman 1 byte olmali");  // bu her zaman geçer

int main() {
    // Yerel static_assert de olabilir
    static_assert(sizeof(double) == 8, "double 8 byte olmali");
    
    constexpr int BUFFER_SIZE = 1024;
    static_assert(BUFFER_SIZE > 0, "Buffer boyutu pozitif olmali");
    static_assert(BUFFER_SIZE % 4 == 0, "Buffer boyutu 4'un kati olmali");
    
    std::cout << "Tum kontroller gecti!" << std::endl;
    return 0;
}

static_assert bir header gerektirmez — dil özelliğidir (language feature), makro değil. Herhangi bir #include olmadan kullanabilirsin.

Template'lerde Tip Kontrolü

static_assert en çok template'lerde parlar. Bir template her türle çalışabilir, ama bazen sadece belirli türlerle çalışmasını istersin. static_assert ile bunu derleme zamanında garanti edersin.

#include <iostream>
#include <type_traits>

template <typename T>
T topla(T a, T b) {
    // Sadece aritmetik türler (int, double, float vb.) kabul et
    static_assert(std::is_arithmetic<T>::value,
                  "topla() sadece sayisal turlerle calisir");
    return a + b;
}

template <typename T>
void guvenliKopyala(T* hedef, const T* kaynak, int adet) {
    static_assert(std::is_trivially_copyable<T>::value,
                  "Bu fonksiyon sadece trivially copyable turler icindir");
    for (int i = 0; i < adet; ++i) {
        hedef[i] = kaynak[i];
    }
}

int main() {
    std::cout << topla(3, 5) << std::endl;       // OK
    std::cout << topla(3.14, 2.86) << std::endl;  // OK
    // topla(std::string("a"), std::string("b"));  // DERLEME HATASI!
    
    return 0;
}

<type_traits> header'ı, türler hakkında derleme zamanında soru sormana yarayan araçlar sunar: is_arithmetic, is_pointer, is_integral, is_floating_point ve daha fazlası.

C++17: Mesajsız static_assert

C++17 ile birlikte, hata mesajı vermek artık zorunlu değil. Bazen ifade zaten yeterince açıklayıcıdır:

// C++11/14: mesaj zorunlu
static_assert(sizeof(int) == 4, "int must be 4 bytes");

// C++17: mesaj opsiyonel
static_assert(sizeof(int) == 4);  // mesajsız da olur

Ama benim tavsiyem: her zaman mesaj yaz. Üç ay sonra o hatayı gördüğünde, mesaj olmadan neyin yanlış olduğunu anlamak daha zor olacak.

💡 İpucu: assert() ile static_assert arasındaki seçim basit: ifaden constexpr (derleme zamanında hesaplanabilir) mi? → static_assert. Çalışma zamanına mı bağlı (kullanıcı girdisi, dosya durumu, pointer değeri)? → assert().


Defensive Programming (Savunmacı Programlama)

Felsefe

Savunmacı programlama, "her şey yolunda gidecek" yerine "bir şeyler yanlış gidebilir" varsayımıyla kod yazmaktır. Üç temel kavramı vardır: precondition, postcondition ve invariant.

Bunu bir restoran mutfağına benzetebilirsin. Precondition: Aşçı malzemeleri kontrol eder — tavuk taze mi, baharat yeterli mi? (Fonksiyona giriş kontrolü.) Postcondition: Yemek servise çıkmadan önce tadına bakılır — tuz tamam mı, pişmiş mi? (Fonksiyon çıkış kontrolü.) Invariant: Mutfak her zaman hijyenik olmalı — yemek hazırlanırken de, temizlik yapılırken de. (Sınıfın her an geçerli durumda olması.)

Preconditions (Ön Koşullar)

Fonksiyonun başında, parametrelerin beklenen aralıkta olduğunu doğrula. "Bu fonksiyonu çağıran kişi sözleşmeye uydu mu?" sorusunun cevabıdır.

#include <cassert>
#include <cmath>
#include <iostream>

// Karekök hesapla — negatif sayı kabul etmez
double karekokHesapla(double sayi) {
    assert(sayi >= 0.0 && "karekokHesapla: negatif sayi verildi");
    return std::sqrt(sayi);
}

// Diziden eleman getir — geçerli index gerektirir
int elemanGetir(const int* dizi, int boyut, int index) {
    assert(dizi != nullptr && "elemanGetir: null dizi");
    assert(boyut > 0 && "elemanGetir: boyut pozitif olmali");
    assert(index >= 0 && index < boyut && "elemanGetir: index sinir disi");
    return dizi[index];
}

int main() {
    double sonuc = karekokHesapla(25.0);
    std::cout << "Karekök: " << sonuc << std::endl;
    
    int veriler[] = {10, 20, 30, 40, 50};
    std::cout << "Eleman: " << elemanGetir(veriler, 5, 2) << std::endl;
    
    return 0;
}

assert(koşul && "mesaj") kalıbına dikkat et. String literal her zaman true olduğundan, && operatörü koşulun değerini değiştirmez. Ama hata mesajında string görünür — süper pratik bir hile.

Postconditions (Son Koşullar)

Fonksiyonun sonunda, döndüreceğin değerin mantıklı olduğunu doğrula. "Sözleşmemin benim tarafımı tuttum mu?" sorusudur.

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

// Vektörü sırala ve sıralı olduğunu doğrula
void guvenliSirala(std::vector<int>& vec) {
    // Precondition: özel bir şey yok, boş vektör de olabilir
    
    std::sort(vec.begin(), vec.end());
    
    // Postcondition: vektör gerçekten sıralı mı?
    assert(std::is_sorted(vec.begin(), vec.end()) && 
           "guvenliSirala: siralama basarisiz");
}

// İki pozitif sayıyı çarp, taşma kontrolü yap
int guvenliCarpim(int a, int b) {
    assert(a > 0 && b > 0 && "guvenliCarpim: pozitif sayilar gerekli");
    
    int sonuc = a * b;
    
    // Postcondition: taşma olmadığını kontrol et
    assert(sonuc / a == b && "guvenliCarpim: integer overflow!");
    
    return sonuc;
}

int main() {
    std::vector<int> sayilar = {5, 2, 8, 1, 9, 3};
    guvenliSirala(sayilar);
    
    for (int s : sayilar) std::cout << s << " ";
    std::cout << std::endl;
    
    std::cout << guvenliCarpim(100, 200) << std::endl;
    return 0;
}

Invariants (Değişmezler)

Invariant, bir nesnenin her zaman — yapım (construction) anından yıkım (destruction) anına kadar — geçerli olması gereken bir koşuldur. Genellikle sınıflarda kullanılır.

#include <cassert>
#include <iostream>
#include <string>

class BankaHesabi {
private:
    std::string sahibi;
    double bakiye;
    
    // Invariant kontrol fonksiyonu
    void invariantKontrol() const {
        assert(!sahibi.empty() && "Hesap sahibi bos olamaz");
        assert(bakiye >= 0.0 && "Bakiye negatif olamaz");
    }
    
public:
    BankaHesabi(const std::string& isim, double baslangic)
        : sahibi(isim), bakiye(baslangic) {
        invariantKontrol();  // yapım sonrası kontrol
    }
    
    void paraYatir(double miktar) {
        assert(miktar > 0 && "Yatirilan miktar pozitif olmali");
        bakiye += miktar;
        invariantKontrol();  // işlem sonrası kontrol
    }
    
    bool paraCek(double miktar) {
        assert(miktar > 0 && "Cekilen miktar pozitif olmali");
        if (miktar > bakiye) return false;  // yetersiz bakiye — normal durum
        bakiye -= miktar;
        invariantKontrol();  // işlem sonrası kontrol
        return true;
    }
    
    double getBakiye() const { return bakiye; }
};

int main() {
    BankaHesabi hesap("Ahmet", 1000.0);
    hesap.paraYatir(500.0);
    std::cout << "Bakiye: " << hesap.getBakiye() << std::endl;
    
    if (hesap.paraCek(200.0)) {
        std::cout << "Para cekildi. Bakiye: " << hesap.getBakiye() << std::endl;
    }
    return 0;
}

paraCek fonksiyonundaki ince ayrıma dikkat et: yetersiz bakiye bir programcı hatası değil, normal bir senaryo. Bu yüzden if ile ele alınıyor ve false döndürülüyor. Ama bakiyenin negatife düşmesi bir programcı hatası — bunu assert ile yakalıyoruz.


Basit Test Düşüncesi

Test Nedir?

Test, en basit haliyle şudur: bir fonksiyona bilinen bir girdi ver, çıktının beklediğin gibi olup olmadığını kontrol et. Hepsi bu. Karmaşık framework'ler, süslü araçlar — bunların hepsi sonuçta bu basit fikrin üzerine inşa edilmiş.

Bunu yemek tarifini denemeye benzetebilirsin. Tarifi uygularsın, sonucu tadarsın. Beklediğin gibi mi? Evet → tarif doğru. Hayır → bir yerde hata var. Test de aynı: fonksiyonu çağırırsın, sonucu kontrol edersin.

Test Fonksiyonu Yazma

En basit test yaklaşımı: her fonksiyon için bir test fonksiyonu yaz, assert kullan.

#include <cassert>
#include <iostream>
#include <string>
#include <cmath>

// ---- Test edilecek fonksiyonlar ----

int faktoriyel(int n) {
    if (n <= 1) return 1;
    return n * faktoriyel(n - 1);
}

bool asal_mi(int n) {
    if (n < 2) return false;
    for (int i = 2; i * i <= n; ++i) {
        if (n % i == 0) return false;
    }
    return true;
}

double ucgenAlani(double taban, double yukseklik) {
    return (taban * yukseklik) / 2.0;
}

// ---- Test fonksiyonları ----

void testFaktoriyel() {
    assert(faktoriyel(0) == 1);
    assert(faktoriyel(1) == 1);
    assert(faktoriyel(5) == 120);
    assert(faktoriyel(10) == 3628800);
    std::cout << "  [OK] faktoriyel testleri gecti" << std::endl;
}

void testAsalMi() {
    assert(asal_mi(2) == true);
    assert(asal_mi(3) == true);
    assert(asal_mi(4) == false);
    assert(asal_mi(17) == true);
    assert(asal_mi(1) == false);
    assert(asal_mi(0) == false);
    assert(asal_mi(-5) == false);
    std::cout << "  [OK] asal_mi testleri gecti" << std::endl;
}

void testUcgenAlani() {
    assert(std::abs(ucgenAlani(10.0, 5.0) - 25.0) < 0.001);
    assert(std::abs(ucgenAlani(3.0, 4.0) - 6.0) < 0.001);
    assert(std::abs(ucgenAlani(0.0, 5.0) - 0.0) < 0.001);
    std::cout << "  [OK] ucgenAlani testleri gecti" << std::endl;
}

// ---- Test çalıştırıcı ----

int main() {
    std::cout << "Testler calistiriliyor..." << std::endl;
    
    testFaktoriyel();
    testAsalMi();
    testUcgenAlani();
    
    std::cout << "Tum testler basariyla gecti!" << std::endl;
    return 0;
}

Burada birkaç önemli nokta var:

Sınır değerlerini test et. faktoriyel(0) ve faktoriyel(1) gibi edge case'ler genellikle hataların yuvalandığı yerlerdir. Sadece "normal" değerleri test etmek yeterli değil.

Floating-point karşılaştırma. ucgenAlani testinde == yerine std::abs(a - b) < epsilon kullandık. Ondalık sayılarda tam eşitlik güvenilmez — bu konuyu daha önce görmüştük.

Negatif senaryoları test et. asal_mi(-5) gibi "olmaması gereken" girdileri de test et. Fonksiyonun bunları düzgün ele aldığından emin ol.

Daha İyi Bir Test Pattern'i

Yukarıdaki yaklaşım işe yarar ama biraz ham. Hataları daha güzel raporlayan bir yardımcı yazabiliriz:

#include <iostream>
#include <string>
#include <cmath>

int gecenTest = 0;
int kalanTest = 0;

void kontrol(bool kosul, const std::string& testAdi) {
    if (kosul) {
        std::cout << "  [GECTI] " << testAdi << std::endl;
        gecenTest++;
    } else {
        std::cout << "  [KALDI] " << testAdi << " <<<" << std::endl;
        kalanTest++;
    }
}

// Test edilecek fonksiyon
int mutlakDeger(int n) {
    return (n < 0) ? -n : n;
}

int main() {
    std::cout << "=== mutlakDeger Testleri ===" << std::endl;
    
    kontrol(mutlakDeger(5) == 5,   "pozitif sayi");
    kontrol(mutlakDeger(-5) == 5,  "negatif sayi");
    kontrol(mutlakDeger(0) == 0,   "sifir");
    kontrol(mutlakDeger(-1) == 1,  "negatif bir");
    kontrol(mutlakDeger(100) == 100, "buyuk pozitif");
    
    std::cout << "\nSonuc: " << gecenTest << " gecti, " 
              << kalanTest << " kaldi." << std::endl;
    
    return (kalanTest > 0) ? 1 : 0;  // hata varsa çıkış kodu 1
}

Bu yaklaşımın avantajı: bir test başarısız olduğunda program çökmez, diğer testler de çalışır. Böylece tüm sorunları tek seferde görebilirsin. Ayrıca çıkış kodunu (exit code) kullanarak otomasyon araçlarına "testler geçti mi" bilgisini aktarabilirsin.

💡 İpucu: Gerçek projelerde elle test yazmak yerine Google Test veya Catch2 gibi unit test framework'leri kullanılır. Bu framework'ler test keşfi (test discovery), raporlama, fixture'lar ve çok daha fazlasını sunar. Şu an detaylarını bilmene gerek yok — ama varlıklarından haberdar ol. İleride mutlaka karşına çıkacaklar.

Test Neden Önemlidir?

Birçok yeni programcı testi "gereksiz ekstra iş" olarak görür. Ama gerçek şu: test yazmak başlangıçta zaman alır, ama uzun vadede zaman kazandırır. Çünkü:

  • Güven verir. Kodu değiştirdiğinde, testleri çalıştırarak bir şey kırılıp kırılmadığını anında görürsün.

  • Dokümantasyon görevi görür. Test, fonksiyonun nasıl kullanılması gerektiğini gösterir.

  • Hata bulmayı kolaylaştırır. 1000 satır kodda "bir yerde hata var" demek yerine, hangi testin kaldığına bakarak hatayı daraltabilirsin.

Profesyonel yazılım geliştirmede TDD (Test-Driven Development) denen bir yaklaşım bile var: önce testi yaz, sonra kodu yaz. Ama bu ileri bir konu — şimdilik "her fonksiyon için birkaç test yaz" alışkanlığı edinmek bile seni çoğu programcının önüne geçirir.


assert vs static_assert — Karşılaştırma Tablosu

Özellikassert()static_assert
Kontrol zamanıÇalışma zamanı (runtime)Derleme zamanı (compile time)
Header gerekli mi?Evet (<cassert>)Hayır (dil özelliği)
NDEBUG etkisiRelease'de devre dışıHer zaman aktif
Başarısız olursaProgram çöker (abort)Derleme hatası
Kullanım alanıPointer, index, değer kontrolleriTür boyutları, platform varsayımları, template kısıtları
İfade türüHerhangi bir boolean ifadeconstexpr (derleme zamanında hesaplanabilir) ifade

İkisi birbirinin alternatifi değil, tamamlayıcısıdır. Derleme zamanında kontrol edebildiğin her şeyi static_assert ile kontrol et — böylece hata daha erken yakalanır. Çalışma zamanına bağlı kontroller için assert() kullan.


Gerçek Dünya Senaryosu: Güvenli Bir Yığın (Stack)

Tüm kavramları birleştiren bir örnek yapalım:

#include <cassert>
#include <iostream>
#include <array>

template <typename T, int Kapasite>
class GuvenliYigin {
    static_assert(Kapasite > 0, "Yigin kapasitesi pozitif olmali");
    static_assert(Kapasite <= 10000, "Yigin kapasitesi makul olmali");
    
private:
    std::array<T, Kapasite> veri;
    int ust;  // stack top index
    
    void invariantKontrol() const {
        assert(ust >= 0 && "ust negatif olamaz");
        assert(ust <= Kapasite && "ust kapasiteyi asamaz");
    }
    
public:
    GuvenliYigin() : ust(0) {
        invariantKontrol();
    }
    
    void ekle(const T& eleman) {
        assert(!dolu() && "Dolu yigina eleman eklenemez");
        veri[ust] = eleman;
        ++ust;
        invariantKontrol();
    }
    
    T cikar() {
        assert(!bos() && "Bos yigindan eleman cikarilamaz");
        --ust;
        invariantKontrol();
        return veri[ust];
    }
    
    const T& tepedeki() const {
        assert(!bos() && "Bos yiginda tepedeki eleman yok");
        return veri[ust - 1];
    }
    
    bool bos() const { return ust == 0; }
    bool dolu() const { return ust == Kapasite; }
    int boyut() const { return ust; }
};

// ---- Testler ----
void testGuvenliYigin() {
    GuvenliYigin<int, 5> yigin;
    
    assert(yigin.bos() == true);
    assert(yigin.boyut() == 0);
    
    yigin.ekle(10);
    yigin.ekle(20);
    yigin.ekle(30);
    
    assert(yigin.boyut() == 3);
    assert(yigin.tepedeki() == 30);
    
    int cikarilan = yigin.cikar();
    assert(cikarilan == 30);
    assert(yigin.boyut() == 2);
    
    std::cout << "[OK] GuvenliYigin testleri gecti" << std::endl;
}

int main() {
    testGuvenliYigin();
    return 0;
}

Bu örnekte her şey bir arada: static_assert ile derleme zamanında kapasite kontrolü, assert ile çalışma zamanında precondition/postcondition/invariant kontrolü, ve basit bir test fonksiyonu.


Özet

  • `assert()` çalışma zamanında bir koşulu kontrol eder; koşul false ise programı durdurur. Programcı hatalarını yakalamak için kullanılır, kullanıcı hataları için değil.

  • `NDEBUG` tanımlıyken (release build) tüm assert'ler devre dışı kalır. Bu yüzden assert ifadesinin içine asla yan etki (side effect) olan kod yazma.

  • `static_assert` derleme zamanında kontrol yapar. Tür boyutları, platform varsayımları ve template kısıtları için idealdir. NDEBUG'dan etkilenmez.

  • Defensive programming üç temel üzerine kuruludur: precondition (fonksiyon girişinde kontrol), postcondition (fonksiyon çıkışında kontrol) ve invariant (her zaman geçerli olması gereken koşul).

  • Test yazmak en basit haliyle "bilinen girdi ver, beklenen çıktıyı kontrol et" demektir. Her fonksiyon için birkaç test — özellikle sınır değerleri — yazmak seni çoğu bugdan korur.

  • Gerçek projelerde Google Test veya Catch2 gibi framework'ler kullanılır. Ama assert tabanlı basit testler bile hiç test yazmamaktan kat kat iyidir.