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:
- 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.
- 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ı
- 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.
- Modüller, bir kez derlendikten sonra
- 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.
- İ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.
- 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
- 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.
- Modüller için
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…