ماژولهای C++20
بیاموزید چرا ماژولها جایگزین هدرها میشوند، نحوه اعلان و صادر کردن ماژولها، پارتیشنهای ماژول، و مسیر انتقال از کد مبتنی بر هدر.
چرا ماژولها جایگزین هدرها میشوند
سیستم #include برای دههها پایه و اساس کامپایل C++ بوده است، اما مشکلات جدی دارد. هدرها بهصورت متنی درج میشوند — پیشپردازنده کل فایل را در هر واحد ترجمه کپی میکند. این بدان معناست که #include ممکن است بیش از ۵۰٬۰۰۰ خط کد را در هر فایل .cpp بچسباند. نتیجه: کامپایل کند، ترتیب درج شکننده (ماکروهای یک هدر بر دیگری تأثیر میگذارند)، و نقض قانون یکتعریف (ODR) که باعث رفتار تعریفنشده خاموش میشود.
ماژولهای C++20 تمام این مشکلات را حل میکنند. یک ماژول یکبار به یک رابط باینری کامپایل میشود، سپس مصرفکنندگان بدون تجزیه مجدد آن را وارد (import) میکنند. درج متنی، نشت ماکرو و وابستگی ترتیب درج وجود ندارد. زمان ساخت میتواند ۵ تا ۲۰ برابر برای پروژههای بزرگ بهبود یابد.
اعلان و صادرات ماژول
یک ماژول دارای یک واحد رابط (اعلان آنچه صادر میشود) و بهصورت اختیاری یک یا چند واحد پیادهسازی است. کلیدواژه export اعلانها را برای واردکنندگان قابل مشاهده میکند:
// 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 — 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پارتیشنهای ماژول
ماژولهای بزرگ میتوانند به پارتیشنها تقسیم شوند — زیرماژولهایی که با هم رابط کامل ماژول را تشکیل میدهند. پارتیشنها داخلی ماژول هستند و مصرفکنندگان خارجی نمیتوانند مستقیماً آنها را وارد کنند:
// --- 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 hypotenuseimport 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 چه کاری انجام میدهد؟