Kodax

Concepts و Type Traits — C++ المتقدم

Concepts و Type Traits

القالب template <typename T> يقبل أي نوع، حتى الأنواع التي لا تعمل معه — والخطأ يظهر عميقاً داخل القالب برسالة من عشرات الأسطر. هذا الدرس يعلّمك كيف تسأل عن الأنواع وتضع لها شروطاً.

Type Traits: أسئلة عن الأنواع وقت الترجمة

في <type_traits> قوالب تجيب عن أسئلة مثل "هل T عدد صحيح؟":

| السؤال | الأداة |

|---|---|

| عدد صحيح؟ | is_integral_v<T> |

| عدد عشري؟ | is_floating_point_v<T> |

| رقمي؟ | is_arithmetic_v<T> |

| مؤشر؟ | is_pointer_v<T> |

| نفس النوع؟ | is_same_v<T, U> |

| إزالة const و & | remove_cvref_t<T> (C++20) أو decay_t<T> |

#include <type_traits>
#include <string>

static_assert(std::is_integral_v<int>);
static_assert(!std::is_integral_v<double>);
static_assert(std::is_same_v<std::remove_cvref_t<const std::string&>, std::string>);
// static_assert تفحص وقت الترجمة: إن فشلت لا يُترجم البرنامج

if constexpr: فرع حسب النوع (C++17)

if العادية تترجم الفرعين. if constexpr تترجم الفرع المطابق فقط، فيمكن كتابة كود لا يصح إلا لأنواع معينة:

#include <iostream>
#include <string>
#include <type_traits>
using namespace std;

template <typename T>
string toText(const T& v) {
    if constexpr (is_same_v<T, string>) return "\"" + v + "\"";
    else if constexpr (is_arithmetic_v<T>) return to_string(v);
    else return "<?>";
}

int main() {
    cout << toText(string("Kodax")) << ' ' << toText(42) << '\n';   // "Kodax" 42
}

بدون constexpr سيحاول المترجم ترجمة to_string(v) مع string ويفشل.

Concepts (C++20): شروط بأسماء

concept شرط على نوع، يُكتب بدل typename:

#include <concepts>

template <std::integral T>          // T يجب أن يكون عدداً صحيحاً
T gcdOf(T a, T b) {
    while (b != 0) { T t = a % b; a = b; b = t; }
    return a;
}
// gcdOf(12, 18) → 6
// gcdOf(2.5, 1.0) → خطأ واضح: the constraint 'integral<double>' is not satisfied

concepts قياسية مفيدة: integral، floating_point، same_as، convertible_to، derived_from، invocable، totally_ordered.

كتابة concept بنفسك

requires تصف ما يجب أن يدعمه النوع:

#include <concepts>
#include <cstddef>

template <typename T>
concept HasSize = requires(const T& c) {
    { c.size() } -> std::convertible_to<std::size_t>;   // يجب أن تكون عنده size() تعيد رقماً
};

template <HasSize C>
std::size_t countOf(const C& c) { return c.size(); }
// countOf(std::vector<int>{1,2}) ✅    countOf(5) ❌ رسالة واضحة: int لا يحقق HasSize

أربع صيغ متكافئة

#include <concepts>

template <std::integral T> T twice1(T x) { return x * 2; }
template <typename T> requires std::integral<T> T twice2(T x) { return x * 2; }
template <typename T> T twice3(T x) requires std::integral<T> { return x * 2; }
auto twice4(std::integral auto x) { return x * 2; }      // الصيغة المختصرة

Concepts مقابل الوراثة

concept مثل "واجهة" لكن وقت الترجمة: أي نوع فيه area() يحقق Shape دون أن يرث من شيء، ولا تكلفة vtable. أما الوراثة الافتراضية فتسمح بخلط أنواع مختلفة في حاوية واحدة وقت التشغيل. لكل منهما مكانه.

وماذا عن SFINAE و enable_if؟

قبل C++20 كانت الشروط تُكتب بحيلة تسمى SFINAE مع enable_if — صعبة القراءة. ستراها في مكتبات قديمة؛ لفهمها يكفي أن تعرف أنها تؤدي ما تؤديه concepts اليوم بشكل أوضح.

تمرين

اكتب concept باسم Printable يتطلب أن يعمل std::cout << x، ثم دالة printAll(const vector<T>&) مقيدة به. جرّبها مع vector<int> ثم مع vector من صنف لا يدعم << واقرأ الرسالة.

#include <iostream>
#include <string>
#include <vector>
#include <list>
#include <concepts>
#include <type_traits>
using namespace std;

// ===== 1) Type traits: أسئلة عن الأنواع وقت الترجمة =====
template <typename T>
void describe(const string& name) {
    cout << name << ": ";
    if constexpr (is_integral_v<T>) cout << "عدد صحيح";
    else if constexpr (is_floating_point_v<T>) cout << "عدد عشري";
    else if constexpr (is_pointer_v<T>) cout << "مؤشر";
    else cout << "نوع آخر";
    cout << " (sizeof = " << sizeof(T) << ")\n";
}

// ===== 2) if constexpr: كود مختلف لكل نوع =====
template <typename T>
string toText(const T& value) {
    if constexpr (is_same_v<T, string>) return "\"" + value + "\"";
    else if constexpr (is_same_v<T, bool>) return value ? "true" : "false";
    else if constexpr (is_arithmetic_v<T>) return to_string(value);
    else return "<غير مدعوم>";
}

// ===== 3) concept قياسي =====
template <integral T>
T gcdOf(T a, T b) {
    while (b != 0) {
        T t = a % b;
        a = b;
        b = t;
    }
    return a;
}

// ===== 4) concepts مخصصة =====
template <typename T>
concept HasSize = requires(const T& c) {
    { c.size() } -> convertible_to<size_t>;
};

template <HasSize C>
void printSize(const C& c, const string& name) {
    cout << name << " يحتوي " << c.size() << " عناصر\n";
}

template <typename T>
concept Shape = requires(const T& s) {
    { s.area() } -> convertible_to<double>;
    { s.name() } -> convertible_to<string>;
};

// لا وراثة: أي نوع يملك area() و name() يحقق Shape
struct Square {
    double side;
    double area() const { return side * side; }
    string name() const { return "مربع"; }
};

struct Circle {
    double r;
    double area() const { return 3.14159 * r * r; }
    string name() const { return "دائرة"; }
};

void report(const Shape auto& s) {   // الصيغة المختصرة
    cout << s.name() << ": " << s.area() << "\n";
}

int main() {
    cout << "=== Type traits ===\n";
    describe<int>("int");
    describe<double>("double");
    describe<const char*>("const char*");
    describe<string>("string");

    cout << "\n=== if constexpr ===\n";
    cout << toText(string("Kodax")) << " | " << toText(42) << " | " << toText(true) << "\n";

    cout << "\n=== concepts ===\n";
    cout << "gcd(84, 36) = " << gcdOf(84, 36) << "\n";
    cout << "gcd(10L, 4L) = " << gcdOf(10L, 4L) << "\n";
    // gcdOf(2.5, 1.0);   // خطأ ترجمة: double لا يحقق integral

    printSize(vector<int>{1, 2, 3}, "vector");
    printSize(string("مرحبا"), "string (بايتات UTF-8)");
    printSize(list<double>{1.5, 2.5}, "list");
    // printSize(42, "int");   // خطأ ترجمة: int لا يحقق HasSize

    report(Square{3});
    report(Circle{1});

    static_assert(Shape<Square>);
    static_assert(!Shape<int>);
    cout << "\nكل الشروط تحققت وقت الترجمة ✅\n";
    return 0;
}
اختبر فهمك