رفتن به محتوا

ماژول‌های ‎C++‎20

بیاموزید چرا ماژول‌ها جایگزین هدرها می‌شوند، نحوه اعلان و صادر کردن ماژول‌ها، پارتیشن‌های ماژول، و مسیر انتقال از کد مبتنی بر هدر.

چرا ماژول‌ها جایگزین هدرها می‌شوند

سیستم #include برای دهه‌ها پایه و اساس کامپایل ‎C++‎ بوده است، اما مشکلات جدی دارد. هدرها به‌صورت متنی درج می‌شوند — پیش‌پردازنده کل فایل را در هر واحد ترجمه کپی می‌کند. این بدان معناست که #include ممکن است بیش از ۵۰٬۰۰۰ خط کد را در هر فایل .cpp بچسباند. نتیجه: کامپایل کند، ترتیب درج شکننده (ماکروهای یک هدر بر دیگری تأثیر می‌گذارند)، و نقض قانون یک‌تعریف (ODR) که باعث رفتار تعریف‌نشده خاموش می‌شود.

ماژول‌های ‎C++‎20 تمام این مشکلات را حل می‌کنند. یک ماژول یک‌بار به یک رابط باینری کامپایل می‌شود، سپس مصرف‌کنندگان بدون تجزیه مجدد آن را وارد (import) می‌کنند. درج متنی، نشت ماکرو و وابستگی ترتیب درج وجود ندارد. زمان ساخت می‌تواند ۵ تا ۲۰ برابر برای پروژه‌های بزرگ بهبود یابد.

اعلان و صادرات ماژول

یک ماژول دارای یک واحد رابط (اعلان آنچه صادر می‌شود) و به‌صورت اختیاری یک یا چند واحد پیاده‌سازی است. کلیدواژه export اعلان‌ها را برای واردکنندگان قابل مشاهده می‌کند:

math_utils.cppm
// math_utils.cppm — module interface unit
export module math_utils;    // module declaration

import <cmath>;               // import a header unit

// Exported — visible to importers
export double circle_area(double radius) {
    return std::numbers::pi * radius * radius;
}

export struct Point {
    double x, y;

    double distance_to(const Point& other) const {
        double dx = x - other.x;
        double dy = y - other.y;
        return std::sqrt(dx * dx + dy * dy);
    }
};

// NOT exported — module-private (internal linkage)
double helper_function(double val) {
    return val * val;
}

// Export a namespace — all members become visible
export namespace geometry {
    double triangle_area(double base, double height) {
        return 0.5 * base * height;
    }

    double rectangle_area(double width, double height) {
        return width * height;
    }
}

وارد کردن ماژول‌ها

مصرف‌کنندگان از import برای دسترسی به اعلان‌های صادرشده ماژول استفاده می‌کنند. برخلاف #include، import درج متنی انجام نمی‌دهد — ماکروها و جزئیات پیاده‌سازی از مرزهای ماژول نشت نمی‌کنند:

main.cpp
// main.cpp — imports the module
import math_utils;

#include <iostream>  // can still use #include for non-modularized headers

int main() {
    double area = circle_area(5.0);
    std::cout << "Circle area: " << area << "\n";

    Point p1{0.0, 0.0};
    Point p2{3.0, 4.0};
    std::cout << "Distance: " << p1.distance_to(p2) << "\n";  // 5.0

    std::cout << "Triangle: " << geometry::triangle_area(10, 5) << "\n";

    // helper_function(42);  // ERROR: not exported, not visible

    return 0;
}

// Compile order matters for modules:
// 1. Compile the module interface first:
//    g++ -std=c++20 -fmodules-ts -c math_utils.cppm
// 2. Then compile the consumer:
//    g++ -std=c++20 -fmodules-ts main.cpp -o main

پارتیشن‌های ماژول

ماژول‌های بزرگ می‌توانند به پارتیشن‌ها تقسیم شوند — زیرماژول‌هایی که با هم رابط کامل ماژول را تشکیل می‌دهند. پارتیشن‌ها داخلی ماژول هستند و مصرف‌کنندگان خارجی نمی‌توانند مستقیماً آن‌ها را وارد کنند:

module_partitions.cppm
// --- math_utils-algebra.cppm (partition interface) ---
export module math_utils:algebra;

export double square(double x) { return x * x; }
export double cube(double x) { return x * x * x; }

// --- math_utils-trig.cppm (partition interface) ---
export module math_utils:trig;

import <cmath>;

export double deg_to_rad(double degrees) {
    return degrees * std::numbers::pi / 180.0;
}

export double sin_deg(double degrees) {
    return std::sin(deg_to_rad(degrees));
}

// --- math_utils.cppm (primary module interface) ---
export module math_utils;

// Re-export partitions — consumers see everything
export import :algebra;
export import :trig;

// Additional exports can go here
export double hypotenuse(double a, double b) {
    return std::sqrt(square(a) + square(b));
}

// --- Consumer ---
// import math_utils;  // gets algebra, trig, and hypotenuse

import std (‎C++‎23)

‎C++‎23 عبارت import std; را معرفی می‌کند که کل کتابخانه استاندارد را به‌عنوان یک ماژول وارد می‌کند. این تحولی بزرگ برای سرعت کامپایل است:

import std;  // Everything: iostream, vector, string, algorithm, ...

int main() {
    std::println("Hello from ‎C++‎23 modules!");
    std::vector<int> v{1, 2, 3};
    std::ranges::sort(v);
}

import std; ده‌ها دستور #include را با یک import واحد جایگزین می‌کند که به‌مراتب سریع‌تر کامپایل می‌شود زیرا ماژول کتابخانه استاندارد از پیش کامپایل شده است. همچنین import std.compat; وجود دارد که نام‌های کتابخانه C را در فضای نام سراسری (مانند printf، strlen) نیز فراهم می‌کند.

دام

پشتیبانی کامپایلر هنوز در حال بلوغ است: تا سال ۲۰۲۴، GCC، Clang و MSVC همگی از ماژول‌ها پشتیبانی می‌کنند اما با سطوح مختلف کامل بودن و ادغام سیستم ساخت متفاوت. MSVC بالغ‌ترین پشتیبانی را دارد.

ادغام سیستم ساخت: CMake 3.28+ از ماژول‌ها با ویژگی CXX_MODULE_SETS پشتیبانی می‌کند، اما اکوسیستم (اسکنرهای وابستگی، مدیران بسته) هنوز در حال تکامل است. ترتیب ساخت اکنون اهمیت دارد — رابط‌های ماژول باید قبل از مصرف‌کنندگان کامپایل شوند.

عدم صادرات ماکرو: ماژول‌ها عمداً ماکروهای #define را صادر نمی‌کنند. کدی که به ماکروهای هدرها وابسته است (مانند WIN32_LEAN_AND_MEAN یا NOMINMAX) باید این موضوع را به شکل دیگری مدیریت کند.

راهبرد مهاجرت: با import std; (‎C++‎23) برای جایگزینی includeهای کتابخانه استاندارد شروع کنید، سپس به‌تدریج کد خود را از کتابخانه‌های برگ (leaf) که وابستگی‌های کمی دارند ماژولار کنید.

بهترین شیوه

هنگام اتخاذ ماژول‌ها:

1. رابط‌های ماژول را ناب نگه دارید — فقط آنچه مصرف‌کنندگان نیاز دارند صادر کنید. موجودیت‌های صادرنشده پیوند ماژولی دارند و نمی‌توانند نقض ODR ایجاد کنند.
2. از پارتیشن‌ها برای ماژول‌های بزرگ استفاده کنید — به زیرماژول‌های منطقی تقسیم کنید (مثلاً :types، :algorithms، :io).
3. export namespace را ترجیح دهید — به‌جای اعلان‌های منفرد، فضاهای نام کامل را صادر کنید تا رابط منظم بماند.
4. #include و import را برای یک کتابخانه مخلوط نکنید — یکی را انتخاب کنید و در یک واحد ترجمه ثابت باشید.
5. از پسوندهای .cppm یا .ixx برای فایل‌های رابط ماژول استفاده کنید تا سیستم‌های ساخت بتوانند به‌صورت خودکار آن‌ها را شناسایی کنند.

نکات کلیدی
  • ماژول‌ها #include را با import جایگزین می‌کنند — بدون درج متنی، بدون نشت ماکرو، بدون باگ ترتیب درج
  • از export برای قابل مشاهده کردن اعلان‌ها برای واردکنندگان استفاده کنید؛ موجودیت‌های صادرنشده خصوصی ماژول هستند
  • پارتیشن‌های ماژول، ماژول‌های بزرگ را به زیرمؤلفه‌ها تقسیم می‌کنند و در عین حال رابطی یکپارچه ارائه می‌دهند
  • import std; (‎C++‎23) تمام includeهای کتابخانه استاندارد را با یک import سریع و واحد جایگزین می‌کند
  • سیستم‌های ساخت باید رابط‌های ماژول را قبل از مصرف‌کنندگان کامپایل کنند — ترتیب کامپایل اهمیت دارد

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

(15 امتیاز)

1. مزیت اصلی ماژول‌ها نسبت به #include چیست؟

2. چه اتفاقی برای تابعی می‌افتد که در ماژول تعریف شده اما با `export` علامت‌گذاری نشده است؟

3. `import std;` در ‎C++‎23 چه کاری انجام می‌دهد؟