رفتن به محتوا

constexpr، consteval و برنامه‌نویسی زمان کامپایل

محاسبات را با استفاده از constexpr، consteval و constinit از زمان اجرا به زمان کامپایل منتقل کنید. جداول جستجو بسازید، تنظیمات را اعتبارسنجی کنید و از ظروف constexpr استفاده کنید.

برنامه‌نویسی زمان کامپایل در ++C

++C به‌طور منحصربه‌فردی به شما اجازه می‌دهد کد واقعی را در زمان کامپایل اجرا کنید. این بدان معنی است که کامپایلر می‌تواند مقادیر را محاسبه کند، ورودی‌ها را اعتبارسنجی کند، جداول جستجو بسازد و حتی الگوریتم‌های پیچیده اجرا کند — همه قبل از ارسال برنامه شما. نتیجه مستقیماً در باینری به‌عنوان ثابت جاسازی می‌شود.

سیر تکامل برنامه‌نویسی زمان کامپایل در ++C:

- ‎C++‎11: constexpr معرفی شد — توابع ساده با یک دستور return
- ‎C++‎14: constexpr آزادتر — حلقه‌ها، متغیرهای محلی، دستورات متعدد
- ‎C++‎17: if constexpr — انشعاب زمان کامپایل که مسیرهای انتخاب‌نشده را کنار می‌گذارد
- ‎C++‎20: consteval (باید زمان کامپایل باشد)، constinit (تضمین مقداردهی اولیه ایستا در زمان کامپایل)، ظروف constexpr (vector، string)
- ‎C++‎23: if consteval — تشخیص اینکه آیا در زمینه ارزیابی ثابت هستید

توابع constexpr

یک تابع constexpr می‌تواند در زمان کامپایل یا زمان اجرا ارزیابی شود. اگر با آرگومان‌های زمان کامپایل در زمینه‌ای که به ثابت نیاز دارد فراخوانی شود (آرگومان قالب، متغیر constexpr، static_assert، اندازه آرایه)، کامپایلر آن را در زمان کامپایل ارزیابی می‌کند. در غیر این صورت، مانند تابع معمولی در زمان اجرا اجرا می‌شود.

constexpr_functions.cpp
#include <iostream>
#include <array>
#include <cstdint>

constexpr int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

constexpr int fibonacci(int n) {
    if (n <= 1) return n;
    int a = 0, b = 1;
    for (int i = 2; i <= n; ++i) {
        int next = a + b;
        a = b;
        b = next;
    }
    return b;
}

int main() {
    // Compile-time evaluation: used in constexpr context
    constexpr int fact10 = factorial(10);  // computed at compile time
    static_assert(fact10 == 3628800);

    constexpr int fib10 = fibonacci(10);   // computed at compile time
    static_assert(fib10 == 55);

    // Compile-time array sizes
    std::array<int, factorial(5)> arr{};   // array of 120 elements

    // Runtime evaluation: argument not constexpr
    int n;
    std::cin >> n;
    std::cout << "factorial(" << n << ") = " << factorial(n) << '\n';
    // Same function, now runs at runtime
}

consteval و constinit (‎C++‎20)

consteval تابعی را مشخص می‌کند که باید در زمان کامپایل ارزیابی شود — فراخوانی آن با مقادیر زمان اجرا خطای کامپایل است. به این «تابع فوری» (immediate function) گفته می‌شود. constinit تضمین می‌کند متغیری با مدت ذخیره‌سازی ایستا یا thread-local در زمان کامپایل مقداردهی اولیه شود، و از «فاجعه ترتیب مقداردهی اولیه ایستا» جلوگیری می‌کند — اما بر خلاف constexpr، متغیر می‌تواند پس از مقداردهی اولیه تغییر کند.

consteval_constinit.cpp
#include <iostream>

// consteval: MUST be evaluated at compile time
consteval int square(int x) {
    return x * x;
}

// constinit: must be initialized at compile time,
// but can be modified at runtime
constinit int global_value = square(7);  // 49, computed at compile time

// constexpr variable: initialized at compile time AND immutable
constexpr int fixed_value = square(10);  // 100, compile time, cannot change

int main() {
    constexpr int a = square(5);  // OK: compile-time context
    static_assert(a == 25);

    // int x = 5;
    // int b = square(x);  // ERROR: consteval requires compile-time args

    // constinit variable can be modified after initialization
    global_value = 100;  // OK: runtime modification allowed
    std::cout << global_value << '\n'; // 100

    // fixed_value = 200;  // ERROR: constexpr variable is const
}

if constexpr در عمل

if constexpr (‎C++‎17) شرطی را در زمان کامپایل ارزیابی کرده و شاخه انتخاب‌نشده را کاملاً کنار می‌گذارد. شاخه کنار گذاشته‌شده نمونه‌سازی نمی‌شود — لازم نیست برای آرگومان‌های قالب داده‌شده کد معتبری باشد. این اساساً متفاوت از if معمولی است، جایی که هر دو شاخه باید معتبر باشند.

قواعد کلیدی:

1. شرط باید یک عبارت ثابت زمان کامپایل باشد.
2. در یک قالب، شاخه کنار گذاشته‌شده برای آرگومان‌های قالب فعلی نمونه‌سازی نمی‌شود.
3. خارج از قالب، هر دو شاخه باید همچنان از نظر نحوی معتبر باشند (فقط اجرا نمی‌شوند).
4. if constexpr می‌تواند در توابع معمولی هم ظاهر شود، نه فقط قالب‌ها — اما کنار گذاشتن فقط در نمونه‌سازی قالب اعمال می‌شود.
5. هر شاخه if constexpr حوزه خاص خود را دارد — متغیرهای اعلام‌شده در یک شاخه در شاخه دیگر قابل مشاهده نیستند.

ظروف constexpr (‎C++‎20)

‎C++‎20 اجازه استفاده از std::vector و std::string را در زمینه‌های constexpr می‌دهد. می‌توانید ظروف را در زمان کامپایل بسازید، دستکاری و پرس‌وجو کنید — گسترش عظیمی از قابلیت‌های برنامه‌نویسی زمان کامپایل. محدودیت: تخصیص گذرا — حافظه تخصیص‌یافته در حین ارزیابی زمان کامپایل باید قبل از پایان ارزیابی constexpr آزاد شود.

constexpr_containers.cpp
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
#include <array>

// Build and process a vector at compile time (C++20)
constexpr int sum_of_squares(int n) {
    std::vector<int> v;
    for (int i = 1; i <= n; ++i) {
        v.push_back(i * i);
    }
    return std::accumulate(v.begin(), v.end(), 0);
    // vector memory freed here — transient allocation
}

// Compile-time string processing
constexpr std::size_t count_vowels(std::string_view sv) {
    std::size_t count = 0;
    for (char c : sv) {
        if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||
            c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U') {
            ++count;
        }
    }
    return count;
}

// Generate a lookup table at compile time
template <std::size_t N>
constexpr auto generate_squares() {
    std::array<int, N> result{};
    for (std::size_t i = 0; i < N; ++i) {
        result[i] = static_cast<int>(i * i);
    }
    return result;
}

static_assert(sum_of_squares(5) == 55); // 1+4+9+16+25
static_assert(count_vowels("Hello World") == 3);

constexpr auto squares = generate_squares<10>();
static_assert(squares[7] == 49);

برنامه‌نویسی عملی زمان کامپایل

محاسبات زمان کامپایل فقط یک نمایش نیست. کاربردهای دنیای واقعی شامل: ساخت جداول جستجو جاسازی‌شده در باینری، اعتبارسنجی تنظیمات در زمان کامپایل، محاسبه هش‌ها برای تطبیق رشته زمان کامپایل، و تولید مسیرهای کد بهینه‌شده است.

practical_constexpr.cpp
#include <array>
#include <cstdint>
#include <stdexcept>

// Compile-time CRC32 table generation
constexpr auto generate_crc32_table() {
    std::array<uint32_t, 256> table{};
    for (uint32_t i = 0; i < 256; ++i) {
        uint32_t crc = i;
        for (int j = 0; j < 8; ++j) {
            crc = (crc >> 1) ^ (0xEDB88320 * (crc & 1));
        }
        table[i] = crc;
    }
    return table;
}

constexpr auto crc32_table = generate_crc32_table();

// Compile-time configuration validation
struct Config {
    int port;
    int max_connections;
    int timeout_ms;
};

consteval Config validated_config(int port, int max_conn, int timeout) {
    if (port < 1 || port > 65535)
        throw "Port out of range";  // compile error if invalid!
    if (max_conn < 1 || max_conn > 10000)
        throw "Invalid max connections";
    if (timeout < 0)
        throw "Negative timeout";
    return {port, max_conn, timeout};
}

// This validates at compile time — typo or bad value = build fails
constexpr auto config = validated_config(8080, 1000, 5000); // OK
// constexpr auto bad = validated_config(99999, 1000, 5000); // ERROR!
دام

اشتباهات رایج با برنامه‌نویسی زمان کامپایل:

1. فرض اینکه constexpr فقط زمان کامپایل است — یک تابع constexpr می‌تواند در زمان اجرا هم اجرا شود. فقط consteval ارزیابی زمان کامپایل را تضمین می‌کند.
2. تخصیص گذرا — در زمینه‌های constexpr در ‎C++‎20، تمام حافظه تخصیص‌یافته در حین ارزیابی باید قبل از تکمیل ارزیابی آزاد شود. نمی‌توانید یک constexpr std::vector را به‌عنوان متغیر سراسری ذخیره کنید (حافظه heap آن باقی می‌ماند).
3. همه توابع constexpr-سازگار نیستند — ورودی/خروجی، ایجاد نخ، reinterpret_cast و اسمبلی درون‌خطی نمی‌توانند در زمینه‌های constexpr ظاهر شوند.
4. استفاده بیش از حد از constexpr — محاسبه همه چیز در زمان کامپایل زمان ساخت را افزایش می‌دهد. آن را برای مقادیری که واقعاً از ثابت بودن در زمان کامپایل بهره می‌برند نگه دارید.

نکات کلیدی
  • توابع constexpr بسته به زمینه در زمان کامپایل یا زمان اجرا اجرا می‌شوند
  • consteval (‎C++‎20) ارزیابی فقط زمان کامپایل را اجبار می‌کند — آرگومان‌های زمان اجرا خطای کامپایل هستند
  • constinit (‎C++‎20) مقداردهی اولیه زمان کامپایل متغیرهای ایستا/thread-local را تضمین می‌کند اما اجازه تغییر در زمان اجرا را می‌دهد
  • if constexpr شاخه انتخاب‌نشده را کنار می‌گذارد که لازم نیست برای آرگومان‌های قالب داده‌شده معتبر باشد
  • ‎C++‎20 اجازه استفاده از std::vector و std::string را در زمینه‌های constexpr با تخصیص گذرا می‌دهد
  • کاربردهای عملی شامل تولید جدول جستجو، اعتبارسنجی تنظیمات و هش‌سازی زمان کامپایل است

آزمون — دانش خود را بسنجید

(15 امتیاز)

1. تفاوت بین `constexpr` و `consteval` چیست؟

2. آیا می‌توانید یک `constexpr std::vector<int>` به‌عنوان متغیر سراسری در ‎C++‎20 داشته باشید؟

3. `constinit` چه چیزی را تضمین می‌کند؟