İçindekiler
C++ şablonları güçlüdür; ancak yanlış bir tür kullandığınızda derleyici bazen küçük bir hatayı yüzlerce satırlık bir destana dönüştürür. C++20 ile gelen Concepts, şablon parametrelerinden ne beklendiğini açıkça tanımlayarak bu sorunu büyük ölçüde çözer. Böylece hem derleyici daha anlaşılır hata verir hem de fonksiyonun kabul ettiği türler kaynak koddan okunabilir. ``
Sorun: Şablonlar Fazla İyimserdir
Klasik bir şablon, kendisine verilen türün gerekli işlemleri desteklediğini baştan doğrulamaz:
template <typename T>
T topla(const T& a, const T& b) {
return a + b;
}
Bu fonksiyon T türünün + operatörüne sahip olduğunu varsayar. topla(3, 5) sorunsuzdur; fakat toplama desteklemeyen bir sınıf kullanılırsa hata çoğunlukla fonksiyonun çağrıldığı yeri değil, şablonun derinliklerindeki başarısız işlemleri anlatır.
Concept yaklaşımında şablonun geçerli olma koşulunu bir mantık önermesi gibi düşünebiliriz:
\[Geçerli(T) = Toplanabilir(T) \land SonuçUyumlu(T)\]Koşul false olduğunda derleyici fonksiyon gövdesine dalmadan adayı reddeder. Buna constraint satisfaction, yani kısıtın sağlanması denir.
| Geleneksel şablon | Concept kullanan şablon |
|---|---|
| Beklentiler fonksiyon gövdesinde gizlidir | Beklentiler imzada görünür |
| Hata geç ve dolaylı oluşabilir | Hata çağrı noktasına yakın oluşur |
| Dokümantasyon ayrıca yazılmalıdır | Kısıt aynı zamanda dokümantasyondur |
| Uygunluk deneme yoluyla anlaşılır | Uygunluk derleme zamanında sorgulanır |
İlk Concept’imizi Yazalım
Bir türün + işlemini desteklemesini ve sonucun yine aynı türe çevrilebilmesini isteyelim:
#include <concepts>
// T için gerekli sözdizimini derleme zamanında denetler.
template <typename T>
concept Toplanabilir = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>;
};
template <Toplanabilir T>
T topla(const T& a, const T& b) {
return a + b;
}
requires ifadesi içindeki { a + b }, işlemin geçerli olup olmadığını sınar. Ok işaretinden sonraki std::convertible_to<T> ise sonucun T türüne dönüştürülebilmesini şart koşar. Bu kod işlemi çalıştırmaz; yalnızca sözdizimini ve tür ilişkisini derleme sırasında inceler.
Aynı kısıt farklı biçimlerde uygulanabilir:
template <typename T>
requires Toplanabilir<T>
T ikiKat(const T& değer) {
return değer + değer;
}
İlk yazım kısa ve okunaklıdır. requires cümleciği kullanılan ikinci yazım ise birden fazla koşulu birleştirirken daha esnektir:
template <typename T>
requires Toplanabilir<T> && std::default_initializable<T>
T güvenliToplam(const T& a, const T& b) {
return a + b;
}
Burada tür hem toplanabilir hem de varsayılan biçimde oluşturulabilir olmalıdır. Mantıksal karşılığı şöyledir:
\[Kabul(T) = Toplanabilir(T) \land VarsayılanOluşturulabilir(T)\]Metot Varlığını Denetlemek
Concepts yalnızca operatörleri değil, üye fonksiyonları da kontrol edebilir. Örneğin bir koleksiyonun size() metoduna sahip olmasını isteyelim:
#include <concepts>
#include <cstddef>
template <typename T>
concept Boyutlanabilir = requires(const T& nesne) {
{ nesne.size() } -> std::convertible_to<std::size_t>;
};
template <Boyutlanabilir T>
void boyutuYazdır(const T& nesne) {
std::cout << nesne.size() << '\n';
}
Bu tanım, “Herhangi bir T kabul ederim” yerine “size() çağrılabilen ve sonucu boyut değerine çevrilebilen bir T kabul ederim” der. Niyet artık hem kullanıcıya hem derleyiciye açıktır.
Hazır Concept’leri Unutmayın
<concepts> başlığı std::integral, std::floating_point, std::same_as, std::derived_from ve std::convertible_to gibi hazır araçlar sunar. Örneğin yalnızca tam sayıları kabul etmek son derece kolaydır:
template <std::integral T>
T kare(T sayı) {
return sayı * sayı;
}
Concepts, şablonların gücünü azaltmaz; bu gücün giriş kapısına anlaşılır bir güvenlik görevlisi koyar. Sonuç daha kısa hatalar, daha güçlü API sözleşmeleri ve bakım sırasında daha az “Bu derleyici bana ne anlatıyor?” anıdır.
Yorumlar