رفتن به محتوا

مفاهیم و محدودیت‌های ‎C++‎20 (Concepts & Constraints)

الزامات دقیق بر آرگومان‌های قالب را با استفاده از مفاهیم تعریف کنید. کد عمومی تمیزتری با عبارات requires، قالب‌های تابع اختصاری و مفاهیم کتابخانه استاندارد بنویسید.

مشکلی که مفاهیم حل می‌کنند

پیش از مفاهیم، پیام‌های خطای قالب رمزآلود بودند — استفاده از std::sort روی نوعی بدون operator< می‌توانست صفحات متن خطا در عمق پیاده‌سازی کتابخانه استاندارد تولید کند. مفاهیم این مشکل را با اجازه دادن به شما برای بیان الزامات از پیش روی پارامترهای قالب حل می‌کنند.

یک مفهوم (concept) یک گزاره بولی نام‌گذاری‌شده است که در زمان کامپایل ارزیابی می‌شود. بررسی می‌کند آیا یک نوع مجموعه‌ای از الزامات نحوی و معنایی را برآورده می‌کند. وقتی مفهومی برآورده نشود، کامپایلر پیام خطای واضح و مختصری تولید می‌کند که به الزام برآورده‌نشده اشاره دارد — نه دیواری از ردیابی‌های نمونه‌سازی قالب.

مفاهیم در ‎C++‎20 معرفی شدند و مهم‌ترین بهبود در برنامه‌نویسی عمومی از زمان خود قالب‌ها هستند.

تعریف مفاهیم

یک مفهوم با کلمه کلیدی concept و به دنبال آن یک عبارت requires که مشخص می‌کند یک نوع باید چه عملیاتی را پشتیبانی کند، تعریف می‌شود. عبارت requires می‌تواند عبارات معتبر، انواع بازگشتی، انواع تودرتو و موارد بیشتری را بررسی کند.

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
}

عبارات Requires و نحو اختصاری

روش‌های متعددی برای اعمال مفهوم به یک قالب وجود دارد. عبارت requires بعد از لیست پارامتر قالب یا بعد از امضای تابع قرار می‌گیرد. نحو قالب تابع اختصاری از 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 — Derived از Base مشتق شده است
- std::convertible_to — From به‌صورت ضمنی قابل تبدیل به 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 یک گزاره است که bool برمی‌گرداند

مفاهیم تکرارکننده ():
- 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

تحت‌الشمول مفهوم و تشخیص بارگذاری مجدد

وقتی چند بارگذاری مجدد محدودشده مطابقت دارند، کامپایلر از تحت‌الشمول مفهوم (concept subsumption) برای انتخاب خاص‌ترین مورد استفاده می‌کند. مفهوم A مفهوم B را تحت‌الشمول قرار می‌دهد اگر محدودیت‌های A به‌طور منطقی محدودیت‌های B را نتیجه دهند. این امکان «تصفیه» را فراهم می‌کند — مفهوم خاص‌تر بر مفهوم عمومی‌تر غلبه می‌کند.

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 بدون تعریف مفهوم نام‌گذاری‌شده استفاده می‌کنید. 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 عبارات معتبر، انواع بازگشتی و الزامات تودرتو را بررسی می‌کند
  • چهار روش برای اعمال مفاهیم: محدودیت روی پارامتر قالب، عبارت requires، requires دنباله‌دار، یا Concept auto
  • مفاهیم با && و || ترکیب می‌شوند؛ تحت‌الشمول محدودترین بارگذاری مجدد را انتخاب می‌کند
  • کتابخانه استاندارد مفاهیم را در ، و ارائه می‌دهد
  • 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(); }` کلمه کلیدی 'requires' را دو بار دارد؟