Önceki yazılarımda C++11’in modern C++ programlamaya getirdiği yenilikleri inceledik. Bu postlarda genel olarak dile gelen yeni özelliklerini incelerken çoklu iş parçacığı programlama, bellek yönetimi ve akıllı işaretçiler gibi konulara değindik.

Bu yazıda, Modern C++’ın bir nevi en derinlerine doğru inerek derleme zamanı özelliklerini keşfedeceğiz. std::optional, std::variant, constexpr gibi ifadeleri detaylıca inceleyerek derleme zamanı hususunda C++’ın sunduğu olanakları gözler önüne sermeye çalışacağız.

Esasında derleme zamanı hesaplamalar veya derleme zamanında yapılan işlemleri pek sevmemekteyim. Çünkü bu gibi özellikler kodun okunabilirliğini artırıyor gibi görünse bile kodun inceleme ve hata ayıklama süreçlerini zorlaştırabiliyor. Yine de bazı durumlarda bu tür özelliklerin kullanılması nerdeyse zaruriri gibi bir şey. Bu özelliklerin kullanımını macro kullanımına benzetebilirsiniz. Bazı durumlarda kullanmak süreçleri o kadar rahatır ki, keşke daha önce kullanmaya başlasaymışım dersiniz, ancak çok abanırsanız o zaman da “ulan bunu niye yazdım ben?” diye sıklıkla kendinize söversiniz


Cilala ve Parlat: C++11, C++14 ve C++17

Bu noktada şuna da parantez açmamda yarar var, Modern C++ kavramı biraz esnek bir kavramdır. C++11’den sonra gelen standartlar genel olarak Modern C++ olarak adlandırılır. Ancak C++14 ve C++17 standartları genellikle C++11’in üzerine inşa edildiği için bu standartlar genellikle birlikte ele alınır. Bu yazıda da C++14 ve C++17 standartlarını birlikte inceleyeceğiz. Hatta yeri gelecek C++20’yi de inceleyeceğiz. Anlayacağınız üzere işlerin bir noktada ucu kaçmaya başlayacak. Umarım öyle olmaz :)

1. constexpr Fonksiyonları

constexpr fonksiyonları, ki daha öncesinde ufacık değinmiştim, derleme zamanında değerlendirilebilen ve hesaplanabilen fonksiyonlardır. C++11’de tanıtıldı ve C++14’te genişletildi. C++11’de constexpr fonksiyonları yalnızca tek bir ifade ile sınırlıydı. C++14, döngüler ve dallanma gibi daha karmaşık hesaplamalara izin verecek şekilde bu sınırları genişletti.

constexpr fonksiyonları, derleme zamanında hesaplanan değerlerle çalışır ve çalışma zamanında işlem yapmaz. Bu, performansı ciddi anlamda artırır ve çalışma zamanındaki yükü azaltır.

Örnek: Bir constexpr Faktöriyel

constexpr int factorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; ++i) {
        result *= i;
    }
    return result;
}

int main() {
    constexpr int value = factorial(5); // Derleme zamanında hesaplanır
    std::cout << value; // 120 çıktısını verir
    return 0;
}

Bu kod örneğinde factorial fonksiyonu, derleme zamanında hesaplanan bir değer döndürür. Bu, value değişkeninin derleme zamanında hesaplanmasını sağlar. Bir nevi biz bu value değişkenine 120 değerini atamış oluyoruz. Bu da demek oluyor ki, factorial fonksiyonu çalışma zamanında hiçbir işlem yapmıyor.

Şimdi gel de bu özelliği görüp de “bu bilgisayar şeytan icadı şeylerdir” deme.


2. Template Meta Programlama

C++’ın en güçlü özelliklerinden biri olan şablonlar, derleme zamanında hesaplamalar yapmak için kullanılabilir. Esasında Template meta programlama C++11 öncesinde var olan bir kavram olup, C++’da derleme zamanında türler ve değerler üzerinde işlemler yapmak için kullanılan oldukça yararlı bir araçtır.

Bu şablonlar ile farklı türler üzerinde yapılacak işlemler tek bir şablon ile yapılabilir, içeriğine birden fazla türde nesne alabilen ve bunlara aynı işlemleri uygulayabilen sınıflar ve fonksiyonlar tek bir şablon ile yazılıp bu şablondan türetilerek kullanılabilir. Bu sayede kod tekrarı önlenir ve kodun okunabilirliği artar.

Standart Şablon Kütüphanesi (STL), std::vector, std::map ve std::set gibi genel konteynerleri uygulamak için şablonları yoğun şekilde kullanır.

Bunu neden anlatacağım derseniz, Modern C++ ile birlikte template meta programlama daha da güçlendi. Özellikle C++17 ile birlikte gelen if constexpr ifadesi ile birlikte template meta programlama konusunda öyle uç örnekler görmeye başladım ki bu kadar meta programlama yapılmasının ne kadar sağlıklı olduğunu sorgulamaya başladım diyebilirim.

Neyse, şimdilik fonksiyon şablonları ve sınıf şablonlarını anlatayım, ardından variadic template’ler ve template meta programlama konularına gelen yeniliklere değineceğim.

2.1 Sınıf Şablonları

Şablonları belirlerken biz template anahtar kelimesi ile türü belirtiriz. Bu türü belirtirken typename veya class anahtar kelimelerinden birini kullanabiliriz, class derseniz türün bir sınıf olması gerekirken typename ile her türlü tip şablon içerisine verilir.

Örnek: Bir Sınıf Şablonu


template <typename T>
class Box {
public:
    Box(T value) : value(value) {}
    T getValue() const { return value; }
private:

    T value;
};

int main() {
    Box<int> box(42);
    std::cout << box.getValue(); // 42 çıktısını verir
    return 0;
}

2.2 Fonksiyon Şablonları

Sınıf şablonlarının lacivertidir. Fonksiyon şablonları, fonksiyonun parametrelerinin türlerini belirlemek için template anahtar kelimesi ile tanımlanır.

template<typename T>
T add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(5, 3) << std::endl; // int ile çalışır
    std::cout << add(2.5, 1.5) << std::endl; // double ile çalışır
    return 0;
}

2.3 İleri Meta Programlama Özellikleri

2.3.1 Template Specialization (Şablon Özelleştirme)

Şablon özelleştirme, belirli türler için davranışı özelleştirmeye olanak tanır.

Örnek:

template<typename T>
class Printer {
public:
    void print(T value) {
        std::cout << value << std::endl;
    }
};

// const char* için özelleştirme
template<>
class Printer<const char*> {
public:
    void print(const char* value) {
        std::cout << "String: " << value << std::endl;
    }
};

int main() {
    Printer<int> intPrinter;
    Printer<const char*> stringPrinter;

    intPrinter.print(42); // Çıktı: 42
    stringPrinter.print("Özelleştirilmiş Şablon"); // Çıktı: String: Özelleştirilmiş Şablon
    return 0;
}
2.3.2 Değişken Şablonlar (Variadic Templates)

Değişken şablonlar, fonksiyonların veya sınıfların değişken sayıda şablon argümanı kabul etmesine olanak tanır. Katlama ifadeleri olan ... ile parametreler üzerinde işlem yapmayı basitleştirir.

Örnek:

template<typename... Args>
void printAll(Args... args) {
    (std::cout << ... << args) << std::endl;
}

int main() {
    printAll(1, 2, 3, "Merhaba", 4.5); // Çıktı: 123Merhaba4.5
    return 0;
}

2.3.3 Constraints (Kısıtlamalar) ve Concepts (Kavramlar)

C++20 ile beraber şablonlarda yeni bir özellik tanıtıldı. Hani biraz önce bahsetmiştim hatırlarsanız, şablonların class ve typename anahtar kelimeleri ile belirtildiğini. İşte constraints ifadesi ile birlikte artık şablonların türlerini daha kesin bir şekilde belirleyebiliyoruz.

Bu şekilde kesin belirlemelere biz ilgili fonksiyon için en uygun işlev aşırı yüklerini ve şablon özelleştirmelerini seçmek için kullanılabiliyoruz. Bu kesin belirlemeler bir nevi kısıtlamalar olup, şablon argümanlarındaki gereksinimleri, derleme zamanında değerlendirilir ve bir kısıtlama olarak kullanıldığı bir şablonun arayüzünün bir parçası haline gelir:

Concepts ile Örnek:

#include <concepts>

template<std::integral T>
T multiply(T a, T b) {
    return a * b;
}

int main() {
    std::cout << multiply(5, 3) << std::endl; // Geçerli
    // std::cout << multiply(5.0, 3.0) << std::endl; // Hata: Tam sayı türü değil
    return 0;
}

Örneğin biz artık multiplication yapacağımız zaman bu işlemin aritmetik bir işlem olduğunu bildiğimiz için integral verilerini kabul eden bir fonksiyon yazabiliriz. Bu fonksiyonu çağırırken de integral verileri kullanmamız gerektiğini derleme zamanında anlar ve uygunsuz veri tipleri ile çağrı yaparsak derleme zamanında hata alırız.

Concepts, ise adından da anlaşılacağı üzere bir türün belirli bir kavrama uygun olup olmadığını belirlemek için kullanılır. Bu kavramlar, türlerin belirli özelliklere sahip olup olmadığını belirlemek için kullanılır. Örneğin, std::integral kavramı, tamsayı türlerini temsil eder ve std::floating_point kavramı, kayan nokta türlerini temsil eder. Başka bir deyişle, şablon parametrelerini “doğal” ve kolay bir sözdizimi ile kısıtlayabilirsiniz.

Örnek

#include <numeric>
#include <vector>
#include <iostream>
#include <concepts>

template <typename T> 
requires std::integral<T> || std::floating_point<T>
constexpr double Average(std::vector<T> const &vec) {
    const double sum = std::accumulate(vec.begin(), vec.end(), 0.0);        
    return sum / vec.size();
}

int main() {
    std::vector ints { 1, 2, 3, 4, 5};
    std::cout << Average(ints) << '\n';                                      
}

Şimdi bu örnekle üstteki örnek arasında ne değişti de concepts kullanımı ile birlikte daha güçlü bir yapıya sahip oldu diye düşünebilirsiniz. Ben de öyle düşünüyorum, :) Concepts bu noktada bir nevi şablon türlerini kısıtlarken daha okunaklı ve daha özelleştirilebilir bir yapıya sahip olmamızı sağlıyor.

3. Opsiyonel Parametreler: std::optional

std::optional, bir değerin var olup olmadığını temsil etmenin güvenli bir yolunu sağlar. Ham işaretçiler veya özel değerler kullanma ihtiyacını ortadan kaldırır.

Örnek: Boş Olabilir Bir Değer Döndürmek

#include <optional>
#include <iostream>

std::optional<int> findEven(int num) {
    return (num % 2 == 0) ? std::make_optional(num) : std::nullopt;
}

int main() {
    auto result = findEven(5);
    if (result) {
        std::cout << "Bulundu: " << *result;
    } else {
        std::cout << "Bulunamadı";
    }
    return 0;
}

Şimdi bu noktadaki örnek Rust programlama dilindeki Option tipine benziyor. Bu tip, bir değerin var olup olmadığını belirtmek için kullanılır. Eğer bir değer varsa bu değeri döner, yoksa None döner. Bu sayede bir fonksiyonun dönüş değerinin boş olabileceğini belirtmiş oluruz.


4. std::variant: Union’lara Daha Güvenli Alternatif

std::variant, bir değişkenin birden fazla türden birini güvenli bir şekilde tutmasına izin veren bir türdür. Bir nevi union’lar gibidir ama daha güvenlidir deniliyor. Peki neden daha güvenli? Çünkü union’lar tür güvenliği sağlamazlar. Eğer bir union içerisindeki türü yanlış bir şekilde okumaya çalışırsanız bu durumda tanımsız davranışlar oluşabilir. std::variant ise bu tür durumları engellemek için derleme zamanında bir takım kontroller yapar ve eğer yanlış bir tür okumaya çalışırsanız programınız derleme zamanında hata verir.

Örnek: std::variant Kullanımı

#include <variant>
#include <iostream>

int main() {
    std::variant<int, double, std::string> value;
    value = 42;
    std::cout << std::get<int>(value) << std::endl;

    value = 3.14;
    std::cout << std::get<double>(value) << std::endl;

    value = "Merhaba";
    std::cout << std::get<std::string>(value) << std::endl;

    return 0;
}

5. Yapılandırılmış Bağlantılar (Structured Bindings)

Bu özellik, bir yapı (struct), sınıf (class), dizi (array), veya std::tuple gibi bir veri yapısının bileşenlerini kolayca ayrı değişkenlere ayırmak için kullanılır. Python’vari bir şekilde bir veri yapısını parçalayarak, içindeki bileşenlere bir dizi yeni değişken olarak erişim sağlamak için oldukça güzel bir çözümdür kendisi. Bunu yaparken de auto anahtar kelimesi kullanılır ve değişkenler köşeli parantezler [] içine yazılır.

Örnek: Bir Pair’i Ayırmak

#include <tuple>
#include <iostream>

int main() {
    std::pair<int, std::string> data = {42, "Cevap"};
    auto [number, text] = data; // Pair'i ayır
    std::cout << number << ", " << text << std::endl;
    return 0;
}

Örnek: Bir Tuple’ı Ayırmak


std::tuple<int, double, std::string> getValues() {
    return {42, 3.14, "Hello"};
}

int main() {
    auto [x, y, z] = getValues(); // Tuple parçalama
    std::cout << "x: " << x << ", y: " << y << ", z: " << z << '\n';
    return 0;
}

6. Başlatıcılarla if

Son olarak yaşam döngülerini incelerken aklıma gelen şu konudan bahsetmek istedim. Hatırlarsınız for döngüleri için başlatıcılar bulunuyor, hatta bu başlatıcılar range-based for loops konusunda auto olarak da atanabiliyor. Artık C++17 ile birlikte, bir if veya switch ifadesinin içinde doğrudan değişken başlatmaya olanak tanır.

Örnek: Basitleştirilmiş if İfadesi

#include <map>
#include <iostream>

int main() {
    std::map<int, std::string> data = { {1, "Bir"}, {2, "İki"} };
    if (auto it = data.find(2); it != data.end()) {
        std::cout << "Bulundu: " << it->second;
    } else {
        std::cout << "Bulunamadı";
    }
    return 0;
}

Her C++ postunda biraz daha derinlere inmeye çalışıyorum bir yandan da tekerrürden kaçınmaya çalışıyorum. Umarım bu yazıda Modern C++’ın derleme zamanı özelliklerini anlatırken sizi sıkmamışımdır. Sonraki yazılarımda hayal ettiğim dilde gelen hata yakalama özelliklerine girmek, tabi STL Kütüphanesi’ni daha detaylı inceleyeceğim ki hatrı kalmasın, bir ara da Concepts ve Constraints konularına daha detaylı bir şekilde değineceğim, araya da Couroutines ve Modules konularını sıkıştırırım.

Şimdilik hoşça kalın, sağlıcakla kalın…