انتقل إلى المحتوى

C++20 المفاهيم والقيود

تحديد المتطلبات الدقيقة لوسائط القالب باستخدام المفاهيم. اكتب تعليمات برمجية عامة أكثر وضوحًا باستخدام العبارات المطلوبة وقوالب الوظائف المختصرة ومفاهيم المكتبة القياسية.

مفاهيم المشكلة تحل

قبل المفاهيم، كانت رسائل خطأ القالب مشفرة - باستخدامstd::sortعلى نوع لا يحتوي علىoperator<يمكن أن ينتج صفحات من نص الخطأ عميقًا داخل تطبيق المكتبة القياسية. تحل المفاهيم هذه المشكلة من خلال السماح لك بتحديد المتطلبات مقدمًا على معلمات القالب.

المفهوم هو مسند منطقي مسمى يتم تقييمه في وقت الترجمة. يتحقق مما إذا كان النوع يلبي مجموعة من المتطلبات النحوية والدلالية. عندما لا يتم استيفاء المفهوم، ينتج المترجم رسالة خطأ واضحة وموجزة تشير إلى المتطلبات غير المستوفاة - وليس جدارًا من التتبعات الخلفية لإنشاء مثيل للقالب.

تم تقديم المفاهيم في C++20 وتمثل التحسين الأكثر أهمية للبرمجة العامة منذ القوالب نفسها.

تحديد المفاهيم

يتم تعريف المفهوم باستخدام الكلمة الأساسيةconceptمتبوعة بـ يتطلب تعبيرًا الذي يحدد العمليات التي يجب أن يدعمها النوع. يمكن للتعبير المطلوب التحقق من التعبيرات الصالحة وأنواع الإرجاع والأنواع المتداخلة والمزيد.

concept_definition.cpp
#include <concepts>
#include <iostream>
#include <string>

// Define a concept: T must support +, ==, and copy construction
template <typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::convertible_to<T>;  // a+b must return something convertible to T
    { a == b } -> std::convertible_to<bool>; // must be comparable
    requires std::copy_constructible<T>;     // nested requirement
};

// Concept for types that have .size() and .empty()
template <typename T>
concept Sizable = requires(const T& t) {
    { t.size() } -> std::convertible_to<std::size_t>;
    { t.empty() } -> std::convertible_to<bool>;
};

// Use the concept to constrain a template
template <Addable T>
T sum_three(T a, T b, T c) {
    return a + b + c;
}

template <Sizable Container>
void report_size(const Container& c) {
    std::cout << "Size: " << c.size()
              << (c.empty() ? " (empty)" : "") << '\n';
}

int main() {
    std::cout << sum_three(1, 2, 3)       << '\n'; // 6
    std::cout << sum_three(1.5, 2.5, 3.0) << '\n'; // 7.0

    std::string s = "hello";
    report_size(s);  // Size: 5
    std::vector<int> v{1, 2, 3};
    report_size(v);  // Size: 3
}

يتطلب الجمل وبناء الجملة المختصرة

هناك طرق متعددة لتطبيق مفهوم على القالب. تأتي جملة المتطلبات بعد قائمة معلمات القالب أو بعد توقيع الوظيفة. يستخدم بناء جملة قالب الوظيفة المختصرة Concept autoكنوع معلمة، وهو النموذج الأكثر إيجازًا.

requires_clause.cpp
#include <concepts>
#include <iostream>

// Method 1: Concept as template parameter constraint
template <std::integral T>
T gcd(T a, T b) {
    while (b != 0) {
        T temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

// Method 2: requires clause after template parameters
template <typename T>
    requires std::integral<T>
T lcm(T a, T b) {
    return (a / gcd(a, b)) * b;
}

// Method 3: Trailing requires clause
template <typename T>
T absolute(T value) requires std::signed_integral<T> {
    return value < 0 ? -value : value;
}

// Method 4: Abbreviated function template (Concept auto)
void print_integral(std::integral auto value) {
    std::cout << "Integral: " << value << '\n';
}

// Concept auto works for return types too (C++20)
std::integral auto compute() {
    return 42;  // must return an integral type
}

int main() {
    std::cout << gcd(12, 8)       << '\n'; // 4
    std::cout << lcm(12, 8)       << '\n'; // 24
    std::cout << absolute(-42)    << '\n'; // 42
    print_integral(100);                    // Integral: 100
    // print_integral(3.14);  // ERROR: constraint not satisfied
}

تكوين المفهوم والمفاهيم القياسية

يتم تكوين المفاهيم بشكل طبيعي باستخدام && (الاقتران) و || (الانفصال). توفر المكتبة القياسية مجموعة غنية من المفاهيم في :

مفاهيم اللغة الأساسية:
-std::same_as— T وU من نفس النوع
-std::derived_from- مشتق من القاعدة
-std::convertible_to— من قابل للتحويل ضمنيًا إلى إلى
-std::integral،std::floating_point،std::signed_integral

مفاهيم المقارنة:
-std::equality_comparable- يدعم==و!=
-std::totally_ordered— يدعم<،>،<=،>=،==،!=

مفاهيم الكائن:
-std::movable،std::copyable،std::semiregular،std::regular

مفاهيم قابلة للاستدعاء:
-std::invocable- يمكن استدعاء F باستخدام Args
-std::predicate- F عبارة عن منطقية إرجاع مسندة

مفاهيم التكرار ():
-std::input_iterator،std::forward_iterator،std::random_access_iterator،std::contiguous_iterator

مفاهيم النطاق ():
-std::ranges::range،std::ranges::sized_range،std::ranges::random_access_range

استيعاب المفهوم وحل التحميل الزائد

عندما تتطابق عدة أحمال زائدة مقيدة، يستخدم المترجم مفهوم فرعي لاختيار المفهوم الأكثر تحديدًا. المفهوم (أ) يتضمن المفهوم (ب) إذا كانت قيود (أ) تعني منطقيًا قيود (ب). وهذا يسمح بـ "التحسين" - حيث يفوز المفهوم الأكثر تحديدًا على المفهوم الأكثر عمومية.

subsumption.cpp
#include <concepts>
#include <iostream>

// Concept hierarchy: sorted_range is "more specific" than range
template <typename T>
concept Printable = requires(std::ostream& os, const T& t) {
    { os << t };
};

template <typename T>
concept PrintableNumber = Printable<T> && std::integral<T>;

// Less constrained overload
template <Printable T>
void display(const T& value) {
    std::cout << "[generic] " << value << '\n';
}

// More constrained overload — subsumes Printable
template <PrintableNumber T>
void display(const T& value) {
    std::cout << "[number] " << value << '\n';
}

int main() {
    display(42);              // [number] 42 — PrintableNumber is more specific
    display(std::string("hi")); // [generic] hi — only Printable matches
    display(3.14);            // [generic] 3.14 — not integral
}

يتطلب يتطلب نمط

يظهر requires requires غير المعتاد إلى حد ما عند استخدام تعبير مخصص يتطلب تعبيرًا مباشرًا في عبارة يتطلب دون تحديد مفهوم مسمى. يقدمrequiresالأول شرط القيد؛ يبدأrequiresالثاني بالتعبير المطلوب.

requires_requires.cpp
#include <iostream>
#include <string>

// Ad-hoc constraint without naming a concept
template <typename T>
    requires requires(T a, T b) {
        { a + b } -> std::same_as<T>;
    }
T add(T a, T b) {
    return a + b;
}

// Equivalent named concept (preferred for readability)
template <typename T>
concept AddableToSelf = requires(T a, T b) {
    { a + b } -> std::same_as<T>;
};

template <AddableToSelf T>
T add_named(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(1, 2)                << '\n'; // 3
    std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab
    std::cout << add_named(10, 20)        << '\n'; // 30
}
أفضل ممارسة

عند تصميم المفاهيم:

- تسمية المفاهيم حسب النوع، وليس ما تحتاجه الوظيفة - استخدمSortableوليسHasLessThan. يجب أن تلتقط المفاهيم الفئات الدلالية.
- تفضيل مفاهيم المكتبة القياسية — قبل كتابة مفاهيمك الخاصة، تحقق مما إذا كانأوأويحتوي بالفعل على ما تحتاجه.
- حافظ على المفاهيم صغيرة وقابلة للتركيب — اجمع المفاهيم الصغيرة مع&&بدلاً من إنشاء متطلبات متجانسة.
- استخدم المفاهيم المسماة بدلاً منrequires requiresالمخصص — النموذج المسمى أكثر قابلية للقراءة وإعادة الاستخدام.
- توثيق المتطلبات الدلالية في التعليقات — تتحقق المفاهيم من بناء الجملة، وليس الدلالات. إذا كنت تريد أن يكونoperator+تبادليًا، فاذكر ذلك في تعليق.

النقاط الرئيسية
  • تتم تسمية المفاهيم بمسندات منطقية تقيد معلمات القالب، مما يوفر رسائل خطأ واضحة
  • يتحقق تعبيرrequiresمن التعبيرات الصالحة وأنواع الإرجاع والمتطلبات المتداخلة
  • أربع طرق لتطبيق المفاهيم: القيد على معلمة القالب، أو جملة تتطلب، أو تتطلب زائدة، أوConcept auto
  • يتم تكوين المفاهيم باستخدام&&و||؛ يختار subsumption الحمل الزائد الأكثر تقييدًا
  • توفر المكتبة القياسية مفاهيم فيوو
  • requires requiresهو قيد مضمن مخصص - تفضل المفاهيم المسماة لسهولة القراءة

اختبار — اختبر معرفتك

(20 نقطة خبرة)

1. ماذا يعني`void print(std::integral auto value)`؟

2. إذا تم تعريف المفهوم A على أنه`concept A = B<T> && C<T>`، فماذا يحدث عندما يتطابق الحمل الزائد المقيد B مع التحميل الزائد المقيد A؟

3. لماذا يحتوي`requires requires(T a) { a.size(); }`على الكلمة الأساسية "يتطلب" مرتين؟