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 satisfiedconcepts قياسية مفيدة: 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;
}اختبر فهمك