← Kursa Dön
📄 Text · 12 min

Concepts (C++20)

Template'ler C++'ın en güçlü araçlarından biri. Ama bir sorunları var: yanlış tip verdiğinde derleyici hata mesajları korkunç uzunlukta ve anlaşılmaz oluyor. Bir vector<int>'e sort uygulamak istiyorsun ama yanlışlıkla < operatörü olmayan bir tip verdin — derleyici sana sayfalarca hata mesajı gösteriyor ve gerçek sorunun ne olduğunu bulmak bir dedektiflik macerası.

Concepts, bu sorunu çözer. Template parametrelerine kısıtlamalar (constraints) tanımlarsın: "Bu template sadece tamsayı tiplerle çalışır" veya "Bu tip karşılaştırılabilir olmalı" gibi. Yanlış tip verdiğinde, derleyici kısa ve anlaşılır bir mesajla seni uyarır.

Bir iş ilanı analojisi düşün: "Yazılım geliştirici arıyoruz — en az 3 yıl C++ deneyimi, template bilgisi zorunlu." Bu ilan bir concept'tir — adayın (tipin) karşılaması gereken şartları belirtir. Şartları karşılamayan aday başvuramaz, başvursa da net bir ret mesajı alır.


Concepts Öncesi — Sorun

Template'lerde tip kısıtlaması olmadığında neler olur, bir bakalım:

#include <iostream>

template<typename T>
T add(T a, T b) {
    return a + b;  // T'nin + operatörü olması gerekiyor
}

int main() {
    std::cout << add(3, 5) << "\n";        // OK — int + int
    std::cout << add(1.5, 2.5) << "\n";    // OK — double + double

    // add(std::vector<int>{}, std::vector<int>{});
    // Eğer bunu yazsaydın: uzun ve karışık hata mesajı...

    return 0;
}

Bu basit örnekte sorun yok. Ama büyük template kütüphanelerinde (STL dahil) hata mesajları yüzlerce satır olabilir. Concepts bu durumu düzeltir.


Concept Nedir?

Concept, bir tipin karşılaması gereken şartları tanımlayan bir derleme zamanı ifadesidir. Boolean bir değer döner: tip şartları karşılıyorsa true, karşılamıyorsa false.

requires Clause

requires anahtar kelimesi, template parametresine kısıtlama ekler:

#include <concepts>
#include <iostream>

// Sadece tamsayı tipleri kabul et
template<typename T>
    requires std::integral<T>
T add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(3, 5) << "\n";     // OK — int integral
    std::cout << add(10L, 20L) << "\n"; // OK — long integral

    // add(1.5, 2.5);  // HATA! double integral değil
    // Hata mesajı: "constraints not satisfied" — kısa ve anlaşılır

    return 0;
}

Concept Kullanımının Dört Yolu

Aynı kısıtlamayı farklı söz dizimleriyle yazabilirsin:

#include <concepts>
#include <iostream>

// Yol 1: requires clause (en açık)
template<typename T>
    requires std::integral<T>
T add1(T a, T b) { return a + b; }

// Yol 2: Concept doğrudan template parametresinde
template<std::integral T>
T add2(T a, T b) { return a + b; }

// Yol 3: Trailing requires
template<typename T>
T add3(T a, T b) requires std::integral<T> { return a + b; }

// Yol 4: Abbreviated template (auto ile)
auto add4(std::integral auto a, std::integral auto b) {
    return a + b;
}

int main() {
    std::cout << add1(3, 5) << "\n";
    std::cout << add2(3, 5) << "\n";
    std::cout << add3(3, 5) << "\n";
    std::cout << add4(3, 5) << "\n";

    return 0;
}

💡 Hangi söz dizimini kullanmalı? Basit kısıtlamalar için Yol 2 (template<std::integral T>) en okunabilir. Karmaşık kısıtlamalar için Yol 1 (requires ...) daha uygun. Yol 4 (abbreviated) kısa fonksiyonlar için pratik.


Built-in Concepts

C++20, <concepts> header'ında birçok hazır concept sunar. En sık kullanılanlar:

Temel Tip Concepts

#include <concepts>
#include <string>
#include <iostream>

// std::integral — tamsayı tipleri (int, long, short, char, bool...)
template<std::integral T>
void printInt(T x) {
    std::cout << "Tamsayi: " << x << "\n";
}

// std::floating_point — ondalıklı tipleri (float, double, long double)
template<std::floating_point T>
void printFloat(T x) {
    std::cout << "Ondalikli: " << x << "\n";
}

// std::same_as — belirli bir tip mi?
template<std::same_as<std::string> T>
void printString(T s) {
    std::cout << "String: " << s << "\n";
}

int main() {
    printInt(42);
    printInt('A');       // char da integral
    // printInt(3.14);   // HATA!

    printFloat(3.14);
    printFloat(2.71f);
    // printFloat(42);   // HATA!

    printString(std::string("Merhaba"));
    // printString(42);  // HATA!

    return 0;
}

Karşılaştırma ve Sıralama Concepts

#include <concepts>
#include <vector>
#include <algorithm>
#include <iostream>

// std::totally_ordered — <, >, <=, >= destekleyen tipler
template<std::totally_ordered T>
T findMax(const std::vector<T>& data) {
    return *std::max_element(data.begin(), data.end());
}

// std::equality_comparable — == ve != destekleyen tipler
template<std::equality_comparable T>
bool contains(const std::vector<T>& data, const T& value) {
    return std::find(data.begin(), data.end(), value) != data.end();
}

int main() {
    std::vector<int> nums = {5, 3, 8, 1, 9};
    std::cout << "Max: " << findMax(nums) << "\n";

    std::cout << "9 var mi? " << contains(nums, 9) << "\n";

    return 0;
}

Sık Kullanılan Built-in Concepts

ConceptAnlamı
std::integralTamsayı tipleri
std::floating_pointOndalıklı sayı tipleri
std::same_as<T>Tam olarak T tipi
std::convertible_to<T>T'ye dönüştürülebilir
std::derived_from<Base>Base sınıfından türemiş
std::totally_orderedKarşılaştırma operatörleri var
std::equality_comparable== ve != var
std::copyableKopyalanabilir
std::movableTaşınabilir
std::default_initializableVarsayılan constructor var
std::invocable<Args...>Çağrılabilir (callable)

Custom Concept Tanımlama

Kendi concept'lerini tanımlayabilirsin. concept anahtar kelimesi ile bir boolean ifade tanımlarsın:

Basit Custom Concept

#include <concepts>
#include <string>
#include <iostream>

// Sayısal tip — integral veya floating point
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;

template<Numeric T>
T multiply(T a, T b) {
    return a * b;
}

int main() {
    std::cout << multiply(3, 5) << "\n";     // 15
    std::cout << multiply(2.5, 4.0) << "\n"; // 10

    // multiply(std::string("a"), std::string("b"));  // HATA!

    return 0;
}

requires Expression ile

requires ifadesi, bir tipin belirli operasyonları destekleyip desteklemediğini kontrol eder:

#include <iostream>
#include <string>
#include <vector>
#include <concepts>

// T tipinin .size() metodu olmalı ve bu size_t döndürmeli
template<typename T>
concept HasSize = requires(T t) {
    { t.size() } -> std::convertible_to<std::size_t>;
};

// T tipinin operator<< ile yazdırılabilir olması
template<typename T>
concept Printable = requires(std::ostream& os, T t) {
    { os << t } -> std::same_as<std::ostream&>;
};

template<HasSize T>
void printSize(const T& container) {
    std::cout << "Size: " << container.size() << "\n";
}

template<Printable T>
void display(const T& value) {
    std::cout << "Deger: " << value << "\n";
}

int main() {
    std::vector<int> v = {1, 2, 3};
    std::string s = "Merhaba";

    printSize(v);  // Size: 3
    printSize(s);  // Size: 7

    display(42);
    display("Hello");

    // printSize(42);  // HATA! int'in size() metodu yok

    return 0;
}

Birden Fazla Kısıtlama

#include <concepts>
#include <iostream>

// Karmaşık concept — birden fazla şart
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>;    // + operatörü olmalı, aynı tipi döndürmeli
    { a - b } -> std::same_as<T>;    // - operatörü olmalı
};

template<typename T>
concept PrintableNumber = Addable<T> && requires(std::ostream& os, T t) {
    { os << t } -> std::same_as<std::ostream&>;
};

template<PrintableNumber T>
void calculate(T a, T b) {
    std::cout << "Toplam: " << (a + b) << "\n";
    std::cout << "Fark: " << (a - b) << "\n";
}

int main() {
    calculate(10, 3);     // OK
    calculate(2.5, 1.5);  // OK

    return 0;
}

Concept ile Template Constraint

Concepts, template sınıflarını da kısıtlayabilir:

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

template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;

template<Arithmetic T>
class Statistics {
    std::vector<T> data;

public:
    void add(T value) {
        data.push_back(value);
    }

    double mean() const {
        if (data.empty()) return 0;
        return static_cast<double>(
            std::accumulate(data.begin(), data.end(), T{})
        ) / data.size();
    }

    T min() const {
        return *std::min_element(data.begin(), data.end());
    }

    T max() const {
        return *std::max_element(data.begin(), data.end());
    }
};

int main() {
    Statistics<int> intStats;
    intStats.add(10);
    intStats.add(20);
    intStats.add(30);
    std::cout << "Ortalama: " << intStats.mean() << "\n";
    std::cout << "Min: " << intStats.min() << "\n";
    std::cout << "Max: " << intStats.max() << "\n";

    Statistics<double> dblStats;
    dblStats.add(1.5);
    dblStats.add(2.7);
    dblStats.add(3.9);
    std::cout << "Ortalama: " << dblStats.mean() << "\n";

    // Statistics<std::string> strStats;  // HATA! string Arithmetic değil

    return 0;
}

Concept ile Overloading

Aynı isimle farklı kısıtlamalara sahip fonksiyonlar tanımlayabilirsin — en spesifik olan seçilir:

#include <concepts>
#include <iostream>
#include <string>

// Genel versiyon
template<typename T>
void process(T value) {
    std::cout << "Genel: " << value << "\n";
}

// Tamsayılar için özel versiyon
template<std::integral T>
void process(T value) {
    std::cout << "Tamsayi: " << value << " (hex: 0x"
              << std::hex << value << std::dec << ")\n";
}

// Ondalıklı sayılar için özel versiyon
template<std::floating_point T>
void process(T value) {
    std::cout << "Ondalikli: " << std::fixed << value << "\n";
}

int main() {
    process(42);           // Tamsayi: 42 (hex: 0x2a)
    process(3.14);         // Ondalikli: 3.140000
    process("Merhaba");    // Genel: Merhaba

    return 0;
}

Concepts vs SFINAE Karşılaştırma

C++20 öncesinde template kısıtlamaları SFINAE (Substitution Failure Is Not An Error) ile yapılıyordu. SFINAE, std::enable_if ve karmaşık template metaprogramlama gerektiriyordu. Concepts bunu dramatik şekilde basitleştirdi.

SFINAE Yolu (Eski — Karmaşık)

#include <type_traits>
#include <iostream>

// SFINAE ile — okuması zor, yazması zor
template<typename T,
         typename = std::enable_if_t<std::is_integral_v<T>>>
T addOld(T a, T b) {
    return a + b;
}

// İki kısıtlama — daha da karmaşık
template<typename T,
         std::enable_if_t<std::is_integral_v<T> ||
                          std::is_floating_point_v<T>, int> = 0>
T multiplyOld(T a, T b) {
    return a * b;
}

int main() {
    std::cout << addOld(3, 5) << "\n";
    std::cout << multiplyOld(2.5, 4.0) << "\n";
    return 0;
}

Concepts Yolu (Yeni — Temiz)

#include <concepts>
#include <iostream>

// Concepts ile — okunabilir, yazılabilir
template<std::integral T>
T addNew(T a, T b) {
    return a + b;
}

template<typename T>
    requires std::integral<T> || std::floating_point<T>
T multiplyNew(T a, T b) {
    return a * b;
}

int main() {
    std::cout << addNew(3, 5) << "\n";
    std::cout << multiplyNew(2.5, 4.0) << "\n";
    return 0;
}

Karşılaştırma Tablosu

ÖzellikSFINAEConcepts
Okunabilirlik❌ Çok düşük✅ Çok yüksek
Hata mesajları❌ Uzun ve karışık✅ Kısa ve anlaşılır
Öğrenme eğrisi❌ Dik✅ Düşük
İfade gücü✅ Her şeyi yapabilir✅ Her şeyi yapabilir
Derleyici desteği✅ C++11+⚡ C++20+ gerekli
Composability❌ Zor&&, `` ile kolay

⚠️ Concepts, C++20 gerektirir. Derleme sırasında -std=c++20 flag'ını kullanmayı unutma. Eski projeler veya eski derleyiciler concepts desteklemeyebilir — bu durumda SFINAE veya static_assert kullan.


Pratik Örnek: Generic Container Printer

#include <concepts>
#include <iostream>
#include <vector>
#include <list>
#include <set>
#include <map>
#include <string>

// Container concept — begin(), end(), size() olmalı
template<typename T>
concept Container = requires(T t) {
    t.begin();
    t.end();
    t.size();
};

// Key-value container concept
template<typename T>
concept KeyValueContainer = Container<T> && requires(T t) {
    typename T::key_type;
    typename T::mapped_type;
};

// Normal container'ları yazdır
template<Container T>
void print(const T& c) requires (!KeyValueContainer<T>) {
    std::cout << "[";
    bool first = true;
    for (const auto& item : c) {
        if (!first) std::cout << ", ";
        std::cout << item;
        first = false;
    }
    std::cout << "] (size: " << c.size() << ")\n";
}

// Key-value container'ları yazdır
template<KeyValueContainer T>
void print(const T& c) {
    std::cout << "{";
    bool first = true;
    for (const auto& [key, value] : c) {
        if (!first) std::cout << ", ";
        std::cout << key << ": " << value;
        first = false;
    }
    std::cout << "} (size: " << c.size() << ")\n";
}

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5};
    std::list<std::string> l = {"Ali", "Ayse"};
    std::set<int> s = {9, 3, 7, 1, 5};
    std::map<std::string, int> m = {{"x", 1}, {"y", 2}};

    print(v);   // [1, 2, 3, 4, 5] (size: 5)
    print(l);   // [Ali, Ayse] (size: 2)
    print(s);   // [1, 3, 5, 7, 9] (size: 5)
    print(m);   // {x: 1, y: 2} (size: 2)

    // print(42);  // HATA! int bir Container değil

    return 0;
}

Özet

  • Concepts, template parametrelerine derleme zamanı kısıtlamalar tanımlayan C++20 özelliğidir. Hata mesajlarını kısa ve anlaşılır hale getirir.

  • `requires` clause ile kısıtlama eklersin. Dört farklı söz dizimi var — en okunabilir olan template<std::integral T> biçimidir.

  • Built-in concepts (std::integral, std::floating_point, std::same_as, std::totally_ordered vb.) çoğu ihtiyacı karşılar.

  • Custom concept tanımlamak için concept anahtar kelimesi ve requires ifadesi kullanılır. Bir tipin belirli operasyonları destekleyip desteklemediğini kontrol edebilirsin.

  • Concepts, template sınıflarını kısıtlamak ve aynı isimle farklı kısıtlamalara sahip overload'lar tanımlamak için kullanılır.

  • SFINAE'ye göre çok daha okunabilir, öğrenmesi kolay ve hata mesajları net. Yeni projeler için concepts tercih et, eski projeler SFINAE kullanmaya devam edebilir.