Modern C++ anlattığı bu seri yazıların sonuncusunda coroutines, concepts ve modüllere girerek bitirmekten daha güzel bir alternatif olmasa gerek. Bu yazımda C++20 ile gelen ve dişime dokunan en önemli üç özelliği anlatacağım…


C++20, dilin tarihinde en önemli güncellemelerden biridir. Burada çığır açan üç özellik tanıtıldı: concepts ve coroutines kavramları ve modüller. Bu yapılardan concepts ve couroutines kavramlarından daha önce bahsetmiştim hatırlarsanız.

Bu yazıda, bu özelliklerin ne olduğunu, nasıl çalıştığını ve neden C++ için oyun değiştirici olduklarını enine boyuna değerlendireceğiz. Ve ek modüllerin yapılarından bahsedeceğiz.

1. Concepts: Şablon Programlamayı Basitleştirmek

Concepts Nedir?

Daha öncesinde bahsettiğim gibi, Concepts yapısı, şablon parametreleri için kısıtlamalar belirtmenin bir yolunu sağlar. Bu, şablonları daha okunabilir, hata ayıklanabilir ve sağlam hale getirir. Karışık hata mesajlarına bel bağlamak yerine, concepts ile şablon argümanlarına yönelik gereklilikleri doğrudan uygulayabilirsiniz.

Neden Concepts Kullanılmalı?

Şablon kullanımındaki en temel amaç gereksiz yeniden tanımlamaları ortadan kaldırarak birden fazla obje için aynı işlemleri yapan sınıfları tek seferde şablon halinde yazarak türe bağlı olarak yeniden yazmayı derleme aşamasına atarak kullanıcıya daha az kodla daha kompleks işler yapabilme şansı verir. Ancak bir çok hata şablon sınıflarının beklediği değerlerin ne olduğu ile alakalı olarak yaşanır. Daha derleme zamanında alabileceği tüm türler belli olsa bile sınıfın beklediğinin netleştirilmesi, hataların daha erken yakalanmasını sağlayarak belirsizlikleri azalır.

Örneğin sadece toplama işlemini destekleyen türlerle çalışan bir fonksiyon oluşturalım:

template <typename T>
concept Addable = requires(T a, T b) {
    a + b; // T türü toplama işlemini desteklemeli
};

Bunu kullanacağımız zaman,

#include <concepts>
#include <iostream>

template <typename T>
concept Addable = requires(T a, T b) {
    a + b;
};

void addAndPrint(Addable auto a, Addable auto b) {
    std::cout << a + b << std::endl;
}

int main() {
    addAndPrint(3, 4);        // Geçerli
    // addAndPrint(3, "Hi");  // Derleme hatası
    return 0;
}

Şeklinde daha en baştan internal olarak gelen Addable concept’i ile uyumlu değerlerin verilebileceğini belirtiyor ve derleme zamanında net ve uygulanabilir hata mesajları sağlıyor.


2. Coroutines: Asenkronluğu Kolaylaştırmak

Coroutines, okunması ve bakımı daha kolay asenkron kod yazmanıza olanak tanır. Çünkü C++ temelinde asenkron programlamaya uyumlu bir dil değildir, nitekim bu gibi diller örneğin Dart dili görece yeni bir dildir ve C++ bunu kendiyle uyumlandırmak için coroutines sağladı. Bu özellik akışta belirli noktalarda eventlere ihtiyaç duymaksızın duraklama ve devam etme yeteneği sunar, bu da onları G/Ç işlemleri, olay döngüleri ve lazy generator’lar için ideal hale getirir.

Coroutines üç anahtar kelimeyle duraklatma ve devam etme işlemleri yapar:

  • co_await: Beklenen sonuç hazır olana kadar yürütmeyi durdurur.
  • co_yield: Bir değeri döner ve yürütmeyi durdurur.
  • co_return: Coroutine’i sonlandırır ve bir değer döner.

2.1 Basit Coroutine Örneği

Bir dizi sayıyı lazy olarak üreten bir coroutine:

#include <iostream>
#include <coroutine>

struct Generator {
    struct promise_type {
        int current_value;

        Generator get_return_object() {
            return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::terminate(); }
        std::suspend_always yield_value(int value) {
            current_value = value;
            return {};
        }
        void return_void() {}
    };

    std::coroutine_handle<promise_type> handle;

    Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Generator() { if (handle) handle.destroy(); }

    struct iterator {
        std::coroutine_handle<promise_type> handle;
        bool operator!=(std::default_sentinel_t) const { return !handle.done(); }
        void operator++() { handle.resume(); }
        int operator*() const { return handle.promise().current_value; }
    };

    iterator begin() { return {handle}; }
    std::default_sentinel_t end() { return {}; }
};

Generator generateSequence(int start, int end) {
    for (int i = start; i <= end; ++i) {
        co_yield i;
    }
}

int main() {
    for (int value : generateSequence(1, 5)) {
        std::cout << value << " "; // Çıktı: 1 2 3 4 5
    }
    return 0;
}

2.2 co_await ile Asenkron İşlemler

Coroutines, asenkron işlemleri co_await ile senkron gibi gösterir, bu da okunabilirliği ve sürdürülebilirliği artırır.

Bir asenkron sonucu bekleyen bir coroutine:

#include <iostream>
#include <future>
#include <coroutine>

std::future<void> asyncPrint(int value) {
    std::cout << "İşleniyor: " << value << std::endl;
    co_await std::suspend_always{};
    std::cout << "Tamamlandı: " << value << std::endl;
}

int main() {
    auto result = asyncPrint(42);
    result.wait();
    return 0;
}

3. Modüller

Gel gelelim zurnanın zırt dediği yere. C++20 ile tanıtılan modüller (modules) konusu, ilk gördüğümde bu ne la böyle Python olmuş etraf dedim. Ancak temel methodolojiyi anlayınca C++’ta programlama deneyimini kökten değiştiren bir özellik olduğunu anladım.

C++’ın C’den gelen geleneksel #include‘u onlarca yıldır kullanılıyor. Ancak bu sistemle ilgili önemli bazı problemler var birçok dosyanın defalarca işlenmesi gerekiyor, #pragma once veya #define ile birden fazlaca kez işlenen header dosyalarını bir kere tanımlanmasını sağlayabiliyoruz ancak bu yine de header’lerin her sefereinde yeniden gezilmesine neden oluyor. Header dosyalarının bu şekilde gezilerek derlenmesi bazı friendship ilişkileri sebebiyle, yanlışlıkla döngüsel bağımlılıklar çıkabilir veya aynı isimli ancak farklı içerikli header dosyaların kullanmasından kaynaklanan sorunlarla baş başa kalınabilir.

C++20 ile gelen modüller, headerları daha sınırları belirli bir yapıda ele alarak bu sorunları çözmeyi hedefler. Kod organizasyonu ve bağımlılık yönetiminde devrim niteliğinde bir özellik sunar.


Bir modül genelde iki parçadan oluşur:

  1. Modül Arayüzü (module interface): Modülün dışa açtığı API’leri tanımlar. Bunu header’lardaki tanımlamalar olarak düşünebilirsiniz.
  2. Modül Uygulaması (module implementation): Modülün tanımlamalarının gerçekleştirildiği implementasyon bloklarını içerir.

3.1. Modüller Nasıl Tanımlanır?

Bir modül oluşturmak için şu sözdizimini kullanıyoruz:

export module MyModule; // Modül bildirimi

export int add(int a, int b) {  // Export ile dışa açıyoruz
    return a + b;
}

int subtract(int a, int b) {  // Export olmadan yazılan fonksiyonlar yalnızca modül içerisinde kullanılabilir
    return a - b;
}

Bir modülü kullanmak için import sözcüğü kullanılır:

import MyModule;  // Modülü içe aktar

#include <iostream>
int main() {
    std::cout << add(3, 4) << std::endl; // 7
    // subtract(3, 4); // Hata: subtract dışa açık değil
    return 0;
}

3.2. Modüllerin Avantajları

  1. Daha Hızlı Derleme Süreleri:
    • Modüller, bir kez derlendikten sonra #include gibi her derleme sırasında tekrar işlenmez.
    • Büyük projelerde ciddi zaman kazandırır.
  2. Daha Temiz Bağımlılık Yönetimi:
    • Döngüsel bağımlılıklar modüllerde oluşmaz, çünkü modüller açıkça tanımlanmış arayüzler kullanır.
  3. İsim Çakışmalarının Önlenmesi:
    • Her modül kendi kapsama alanında çalıştığı için, global namespace’e gereksiz isim eklenmez.

3.4. Örnek: Büyük Bir Projede Modüller

Modül Arayüzü

Şimdi bu örnekte güzel bir matematik modülü oluşturalım. Öncelikle

MathModule.hpp dosyasında bizim modülümüzün içeriği olacak:

export module MathModule; // modül tanımı

export int multiply(int a, int b); // modüle export edilecek fonksiyonlar
export double divide(double a, double b);
Modül Uygulaması

MathModule.cpp dosyasında ise modüle ait implementasyonları oluşturalım:

module MathModule;

int multiply(int a, int b) {
    return a * b;
}

double divide(double a, double b) {
    if (b == 0) {
        throw std::invalid_argument("Division by zero!");
    }
    return a / b;
}
Kullanımı

main.cpp dosyası:

import MathModule;
#include <iostream>

int main() {
    std::cout << "Multiply: " << multiply(3, 4) << std::endl; // 12
    std::cout << "Divide: " << divide(10, 2) << std::endl;    // 5.0
    return 0;
}

3.5. Uygulama Notları

Modüllere dair hala bazı problemler var: Bunlar:

  • Araç Desteği:
    • Derleyicilerin (gcc, clang, MSVC) modül desteği sürekli gelişiyor. Ancak modüller hala nispeten yeni bir özellik olduğu için eski derleyicilerle uyum sorunları yaşanabilir.
  • Proje Geçişi:
    • Mevcut bir projeyi modüllere geçirme işlemi zaman alabilir. Aslında bir projeyi bir özelliğe geçirme terimine karşıyım ancak modüller büyük projeleri yönetmekte oldukça yararlı olabilir. Bu sebeple özellikle çok sayıda #include kullanan eski kod tabanlarında modülleri kullanmak yararlı olabileceği gibi dikkatli planlama gerekir.
  • Dosya Uzantıları:
    • Modüller için ixx gibi yeni uzantılar öneriliyor, ancak standart bir uzlaşma henüz tam yerleşmiş değil.

3.6. Standart Kütüphane modülleri

C++20 ile standart kütüphane bile modül olarak kullanılabilir. Örneğin:

import std.core;  // C++ standart kütüphane çekirdeğini içe aktar

#include <iostream>
int main() {
    std::cout << "Hello, Modules!" << std::endl;
    return 0;
}

Bitirirken

Esasında ben bu Modern C++ yazılarına başlarken bu kadar uzatacağımı beklemiyordum ancak zaman geçip de eski kod tabanlarında uygulama geliştirdikçe, modern C++’ın özelliklerinin ehemmiyetini bir kere daha anlar oldum.

Burada anlattığım çoğu özellik özellikle bu sayfadakiler henüz desteği yetersiz olan özellikler olsa bile bundan 5 sene sonrasında bu özellikleri daha sık gördükçe “Evet, iyi ki zamanında bunları yazmışım” diyeceğim. O zamana kadar çoktan yeni özellikler çıkmış olacak belki onları da tanıtacağım.

O güzel günlere kadar,

Esen kalın…