مقایسهی سلام و C++ مقایسهی یکی از بزرگترین زبانهای برنامهنویسی تاریخ با زبانی جوان است که عمداً کوچک میماند. C++ یک زبان سیستمی استانداردشدهی ISO است با انتزاعهای بیهزینه (zero-cost)، RAII، قالبها (templates) و چند دهه کامپایلر، کتابخانه و ابزار. زبان برنامهنویسی سلام یک زبان سیستمی با گونههای ایستاست که بدون زبالهروب به کد بومی کامپایل میشود؛ مجموعهی ویژگیهایش آگاهانه کوچکتر است، یک زنجیرهی ابزار دارد و کدش را میشود به انگلیسی یا فارسی نوشت. هر دو فایل اجرایی بومی و سریع تولید میکنند؛ تفاوتشان در این است که برای رسیدن به آن چه اندازه زبان باید یاد بگیرید و نگه دارید.
این نوشته کارهای کوچک یکسانی را کنار هم میگذارد، شیوهی برخورد دو زبان با حافظه، گونههای عام و خطاها را مقایسه میکند، به کارایی اندازهگیریشده نگاه میکند و با فهرستی صادقانه از موقعیتهایی تمام میشود که C++ هنوز انتخاب درست است. هر برنامهی سلامی که اینجا میبینید هنگام ساخت این سایت کامپایل و اجرا میشود و هر نمونهی C++ با GCC در حالت C++23 کامپایل میشود.
سلام و C++ در یک نگاه
| C++ | سلام | |
|---|---|---|
| استاندارد | ISO C++ (C++20، C++23، C++26 در دست تدوین) | یک پیادهسازی متنباز، پیش از نسخهی ۱٫۰ |
| کامپایلرها | GCC، Clang، MSVC و دیگران | کامپایلر خودمیزبان salam |
| تولید کد | مستقیم به کد ماشین | از راه C، از راه LLVM یا با پشتوانهی بومی خودش |
| حافظه | RAII، اشارهگرهای هوشمند، new/delete دستی | مقدارهای روی پشته، ظرفهای هیپ که با آزادکن() و دیرکن آزاد میشوند |
| گونههای عام | قالبها، concept | تابعها و ساختارهای عام با قید میانجی |
| چندریختی | وراثت، تابع مجازی، std::variant | میانجی با پویا (dyn)، جداشمار با داده، جاسازی با شامل (use) |
| خطاها | استثنا، std::expected | خطادار، تلاش، گرفتن خطا، بدون باز کردن پشته |
| ساخت | CMake، Meson، Bazel؛ vcpkg یا Conan برای بستهها | یک فرمان: salam build، salam test، salam add |
| اکوسیستم | عظیم، در هر حوزه | کتابخانهی استاندارد بزرگ، جامعهی کوچک |
معماری: از کد منبع تا کد ماشین
برنامهی C++ هر بار یک واحد ترجمه (translation unit) کامپایل میشود. هر فایل .cpp سرآیندهایش را include میکند، پیشپردازنده آنها را درون فایل میچسباند، قالبها هر جا استفاده شوند نمونهسازی میشوند و یک پیونددهنده فایلهای شیء را به هم وصل میکند. همین مدل دلیل این است که ساخت C++ تا هزاران فایل مقیاسپذیر است، و دلیل اینکه کند است: همان سرآیندها بارها و بارها تجزیه میشوند. ماژولهای C++20 برای همین آمدهاند و پشتیبانی کامپایلرها و سیستمهای ساخت از آنها کمکم از راه میرسد.
سلام یک برنامه را همراه همهی چیزهایی که وارد میکند یکجا کامپایل میکند. کامپایلر، که خودش به زبان سلام نوشته شده، فایل ورودی و همهی بستههایی را که استفاده میکند میخواند، گونهها و بسیاری قاعدهی دیگر را بررسی میکند و سپس برنامه را به یکی از سه پشتوانه (Backend) میسپارد: کد C تولیدشده که به یک کامپایلر C داده میشود (gcc، clang یا tcc همراه)، LLVM IR، یا پشتوانهی بومی خودش که فایلهای شیء را مستقیم مینویسد. سنجشی که پایینتر بررسی میشود سلام را از راه LLVM با -O3 میسازد. نوشتهی سه راه تولید کد سبکسنگینها را توضیح میدهد و کامپایلر سلام چطور کار میکند یک برنامه را در همهی مرحلهها دنبال میکند.
یک پیامد عملی: سلام سرآیند ندارد. فایلها با فراخوانی (include) آورده یا بستهها با واردسازی (import) وارد میشوند، ترتیب تابعها اهمیتی ندارد و یک نماد فقط وقتی بیرون از بستهاش دیده میشود که همگانی (pub) باشد.
گونههای جمع و تطبیق الگو
یک کار تکراری در کد سیستمی، مقداری است که یکی از چند شکل ممکن را دارد. C++ مدرن از std::variant و std::visit استفاده میکند، معمولاً با کمککنندهی «overloaded» تا هر حالت لامبدای خودش را داشته باشد:
#include <numbers>
#include <print>
#include <variant>
#include <vector>
struct Circle { double r; };
struct Rect { double w, h; };
using Shape = std::variant<Circle, Rect>;
template <class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };
double area(const Shape& s) {
return std::visit(overloaded{
[](const Circle& c) { return std::numbers::pi * c.r * c.r; },
[](const Rect& r) { return r.w * r.h; },
}, s);
}
int main() {
std::vector<Shape> shapes{Circle{1.0}, Rect{2.0, 3.0}};
double total = 0.0;
for (const auto& s : shapes) total += area(s);
std::println("{:.2f}", total);
}این کد از نظر گونه امن و کارآمد است و حالتِ جاافتاده خطای کامپایل میدهد، اما برای خواندنش یک قالب کمکی، یک اعلان using متغیرآرگومان و کمی آشنایی با تفکیک overload لازم است. در سلام همین ساختار یک جداشمار با داده و یک همخوان است:
واردسازی تبدیل
واردسازی ریاضی
جداشمار شکل:
دایره(شعاع: اعشار۶۴)
مستطیل(پهنا: اعشار۶۴، بلندی: اعشار۶۴)
پایان
تابع مساحت(ش: شکل): اعشار۶۴:
برگشت همخوان ش:
دایره(شعاع): ریاضی.پی * شعاع * شعاع پایان
مستطیل(پهنا، بلندی): پهنا * بلندی پایان
پایان
پایان
تابع ریشه:
شکلها := [شکل.دایره(۱.۰)، شکل.مستطیل(۲.۰، ۳.۰)]
ناپایا جمع := ۰.۰
هر ش در شکلها:
جمع += مساحت(ش)
پایان
سرچاپ تبدیل.قالب اعشار با دقت(جمع، ۲)
پایانهمخوان باید همهی حالتها را پوشش دهد، میتواند فیلدها را با نام ببندد، لیترالها را بیازماید و نگهبان اضافه کند (دایره(شعاع) اگر شعاع > ۱۰:)، و یک عبارت است، پس مساحت را مستقیم برمیگرداند.
گونههای عام: قالب و concept در برابر گونهی عام مقید
قالبهای C++ خودشان یک زبان زمان کامپایلاند. میتوانند مقدار حساب کنند، گونه انتخاب کنند و برای آرگومانهای خاص تخصیص (specialization) بگیرند؛ همین است که کتابخانههایی مانند Eigen یا کتابخانهی استاندارد ranges را ممکن میکند. conceptها که در C++20 آمدند، بالاخره به قالب اجازه دادند بگوید از آرگومانهایش چه انتظاری دارد. اینجا یک concept یک قالب تابع را مقید میکند و یک کلاس پایهی مجازی کلاسیک، مجموعهای ناهمگون را در زمان اجرا مدیریت میکند:
#include <concepts>
#include <memory>
#include <print>
#include <vector>
template <typename T>
concept HasArea = requires(const T& t) {
{ t.area() } -> std::convertible_to<double>;
};
struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};
struct Square : Shape {
double side;
explicit Square(double s) : side(s) {}
double area() const override { return side * side; }
};
struct Rect : Shape {
double w, h;
Rect(double w, double h) : w(w), h(h) {}
double area() const override { return w * h; }
};
template <HasArea T>
const T& larger(const T& a, const T& b) {
return a.area() >= b.area() ? a : b;
}
int main() {
std::println("{}", larger(Square{3.0}, Square{2.0}).side);
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Square>(2.0));
shapes.push_back(std::make_unique<Rect>(2.0, 3.0));
double total = 0.0;
for (const auto& s : shapes) total += s->area();
std::println("{}", total);
}سلام برای هر دو حالت یک سازوکار دارد. یک میانجی (interface) متدها را فهرست میکند، ساختاری که آن متدها را داشته باشد میانجی را برآورده میکند (بدون اعلان وراثت)، قیدی مانند <ت: مساحتدار> تابعی عام میسازد که در زمان کامپایل برای هر گونه جداگانه ساخته میشود، و پویا مساحتدار برای مجموعهی ناهمگون، فراخوانی پویا (dynamic dispatch) فراهم میکند:
میانجی مساحتدار:
تابع مساحت(): اعشار۶۴
پایان
ساختار مربع:
همگانی ضلع: اعشار۶۴
همگانی تابع مساحت(): اعشار۶۴: برگشت این.ضلع * این.ضلع پایان
پایان
ساختار مستطیل:
همگانی پهنا: اعشار۶۴
همگانی بلندی: اعشار۶۴
همگانی تابع مساحت(): اعشار۶۴: برگشت این.پهنا * این.بلندی پایان
پایان
تابع پرمساحتتر<ت: مساحتدار>(الف: ت، ب: ت): ت:
اگر الف.مساحت() >= ب.مساحت():
برگشت الف
پایان
برگشت ب
پایان
تابع ریشه:
سرچاپ پرمساحتتر(مربع { ضلع = ۳.۰ }، مربع { ضلع = ۲.۰ }).ضلع
شکلها := [مربع { ضلع = ۲.۰ }، مستطیل { پهنا = ۲.۰، بلندی = ۳.۰ }] برگردان پویا مساحتدار[۲]
ناپایا جمع := ۰.۰
هر ش در شکلها:
جمع += ش.مساحت()
پایان
سرچاپ جمع
پایانآنچه سلام ندارد هم به همان اندازه مهم است: فرابرنامهنویسی با قالب، تخصیص قالب، قالبهای متغیرآرگومان و سلسلهمراتب وراثت. استفادهی دوباره از کد میان ساختارها از راه جاسازی (شامل پایه، که فیلدها و متدهای ساختار جاسازیشده را بالا میآورد) و میانجیهایی با متد پیشفرض انجام میشود. سلام بازتعریف عملگر، گونههای متمایز مانند گونه متر: صحیح (type Meters: int)، ثابتهای زمان کامپایل و کامپایل شرطی دارد (درس گونههای عام و کامپایل شرطی را ببینید). اگر طراحی شما به محاسبهی سنگین زمان کامپایل در سیستم گونه وابسته است، C++ چیزهایی را بیان میکند که سلام نمیتواند.
مدیریت منابع: RAII در برابر دیرکن
این بزرگترین تفاوت عملی است و C++ در آن جلوتر است. در C++ وقتی شیئی از محدوده خارج میشود مخربش (destructor) اجرا میشود، پس یک std::vector، یک std::map یا یک جریان فایل در هر مسیری، از جمله استثناها، خودش را جمع میکند. شمارش واژهها کوتاه است:
#include <iostream>
#include <map>
#include <sstream>
#include <string>
int main() {
std::istringstream in("the cat saw the dog and the dog saw the cat");
std::map<std::string, int> counts;
for (std::string w; in >> w;) ++counts[w];
for (const auto& [word, n] : counts) std::cout << word << ' ' << n << '\n';
}سلام مخرب ندارد. مقدارهای روی پشته (عددها، ساختارها، آرایههای ثابت) به کاری نیاز ندارند، اما ظرفهای هیپ مانند وکتور و نگاشت باید آزاد شوند. روش جاافتاده این است که درست بعد از ساختن هر کدام دیرکن ظرف.آزادکن() بنویسید، تا پاکسازی کنار تخصیص بنشیند و در هر راه خروج از بلوک اجرا شود. جمله را همان جملهی انگلیسی نمونهی C++ گذاشتهایم تا خروجیها مقایسهپذیر باشند:
واردسازی مرتب سازی
تابع ریشه:
متن := "the cat saw the dog and the dog saw the cat"
واژهها := متن.بشکاف(" ")
دیرکن واژهها.آزادکن()
ناپایا شمارش := نگاشت {} برگردان نگاشت<رشته، صحیح>
دیرکن شمارش.آزادکن()
هر کلمه در واژهها:
شمارش.درج(کلمه، شمارش.دریافت با پیش فرض(کلمه، ۰) + ۱)
پایان
ناپایا کلیدها := وکتور {} برگردان وکتور<رشته>
دیرکن کلیدها.آزادکن()
هر ک، _ در شمارش:
کلیدها.بیفزا(ک)
پایان
مرتب سازی.مرتب(کلیدها)
هر ک در کلیدها:
سرچاپ ک، شمارش.بگیر(ک)
پایان
پایاننسخهی سلام بلندتر است؛ بخشی به این دلیل که نگاشت به ترتیب درهمسازی پیمایش میشود و کلیدها باید مرتب شوند، و بخشی چون هر ظرف دیرکن خودش را لازم دارد. فراموش کردن یکی از آنها یعنی نشت حافظه. برای کم کردن این احتمال، کامپایلر برخی الگوهای استفاده پس از آزادسازی و آزادسازی دوباره را پیش از اجرای برنامه گزارش میکند، یک بررسیکنندهی مالکیت دربارهی انتقالهای مشکوک هشدار میدهد، و salam memcheck برنامه را زیر Valgrind یا AddressSanitizer اجرا میکند. هیچکدام از اینها اثبات نیست. C++ هم از نظر حافظه امن نیست (ارجاع آویزان، بیاعتبار شدن پیمایشگر و رفتار تعریفنشده همه هنوز ممکناند)، اما RAII رایجترین نوع نشت را از اساس حذف میکند. نوشتهی مدیریت حافظه در سلام و درس حافظه جزئیات بیشتری دارند.
دربارهی خطاها، C++ استثنا دارد که پشته را باز میکند و مخربها را اجرا میکند، و از C++23 هم std::expected برای مقدارهای خطا. خطادار، پرتاب خطا، تلاش و گرفتن خطا در سلام شبیه استثنا خوانده میشوند، اما خطا مثل یک مقدار برگشتی معمولی برمیگردد: چیزی باز نمیشود، دستورهای دیرکن اجرا میشوند، و هر فراخوانیای که ممکن است شکست بخورد باید با تلاش علامت بخورد یا درون یک بلوک تلاش باشد، پس نقطههای احتمالی شکست در کد منبع دیده میشوند.
کارایی: یک رده، اندازهگیریشده
هر دو زبان پیش از اجرا و بدون زبالهروب به کد بومی کامپایل میشوند، پس انتظار دارید نزدیک هم باشند، و سنجش پروژه هم همین را میگوید. روی ۳۹ برنامهای که در همهی زبانها الگوریتم یکسان دارند، در اندازهگیری ۲۷ اکتبر ۲۰۲۶ (۵ آبان ۱۴۰۵)، میانگینهای هندسی برای سلام ۱۰٫۶ میلیثانیه، برای C ۱۱٫۷، برای Rust ۱۱٫۳ و برای C++ ۱۳٫۱ میلیثانیه بود (GCC با -O3 برای C و C++، و سلام از راه پشتوانهی LLVM با -O3). در مقایسه با سریعترینِ C، C++، Rust و Go در هر برنامه، سلام در ۶ برنامه جلوتر، در ۲۷ برنامه برابر و در ۶ برنامه عقبتر بود.
این یعنی سلام از C++ سریعتر نیست. بیشتر این برنامهها در چند میلیثانیه تمام میشوند، پس راهاندازی فرایند جزو هر زمان است، و فاصلههای کمتر از حدود ۵٪ روی ماشین مجازی اشتراکی CI که این عددها را تولید کرد از اجرایی به اجرای دیگر عوض میشوند. خوانش منصفانه این است: «در ردهی C، C++ و Rust» روی برنامههای کوچک محاسباتی. سلام در فیبوناچی بازگشتی، n وزیر، یک مولد همنهشتی خطی (LCG)، یک حلقهی شمارش فشرده، یک آزمون فشار جستجوی دودویی و چاپ یک خط به تعداد یک میلیون بار عقبتر بود. این سنجش برنامههای بزرگ را نمیسنجد، جایی که بهینهسازهای پختهی C++، بهینهسازی هدایتشده با پروفایل و کتابخانههای دستتنظیمشده خیلی به حساب میآیند. صفحهی سرعت همهی عددها و دادهی خام را منتشر میکند و آیا سلام از C و Rust سریعتر است؟ آن را با دقت میخواند.
ابزارها و سیستمهای ساخت
ابزارهای C++ عمیق و پراکندهاند. چند کامپایلر عالی، اشکالزداها و پروفایلرهای درجهیک، sanitizerها، تحلیلگرهای ایستا و IDEها وجود دارند. اما سیستم ساخت یا مدیر بستهی استانداردی وجود ندارد: بیشتر پروژهها از CMake استفاده میکنند، برخی از Meson یا Bazel، و وابستگیها از vcpkg، Conan، مدیر بستهی سیستم یا یک نسخهی کپیشده در مخزن میآیند. اینکه یک پروژهی C++ چندسکویی با وابستگیهایش تمیز ساخته شود، خودش یک مهارت است.
زنجیرهی ابزار سلام یک فایل اجرایی است. salam build کامپایل میکند، salam test آزمونها را اجرا میکند، salam format قالببندی میکند، salam check بدون تولید کد گونهها را بررسی میکند، salam debug با اطلاعات اشکالزدایی میسازد و GDB یا LLDB را باز میکند، salam memcheck حافظه را بررسی میکند و salam add یک بسته را از GitHub میگیرد و نسخهاش را در یک فایل قفل ثبت میکند. کامپایل متقابل یک پرچم --target است. سادهتر است چون کمتر است: پروفایلرها، تحلیلگرها و پشتیبانی IDE پیرامون C++ بسیار پختهترند. نوشتهی زنجیرهی ابزار توسعهی سلام آنچه را امروز هست فهرست میکند.
گسترهی برنامهنویسی سیستمی
تقریباً هر چیزِ حساس به کارایی با C++ اجرا میشود: مرورگرها، موتورهای بازی، پایگاههای داده، کامپایلرها، سامانههای معاملاتی، میانافزار سختافزارهای نهفته و بخش بزرگی از زیرساخت پشت سرویسهای ابری. کتابخانههایش هر حوزهای را پوشش میدهند و تعاملش با C مستقیم است.
سلام بیش از آنچه سنش نشان میدهد به لایههای پایین میرسد. میتواند تابعهای C را در یک بلوک درونداد: (extern:) اعلان و مستقیم صدا بزند، تابعهای سلام را با برونداد: (export:) به C عرضه کند، کتابخانههای اشتراکی یا ایستا را همراه یک سرآیند تولیدشده بسازد (salam build --lib)، و برنامههای مستقل (freestanding) و هستههای کوچکی بسازد که بدون سیستمعامل بوت میشوند (سلام مستقل و بدون سیستمعامل). صدا زدن C++ از سلام، مثل بیشتر زبانها، از راه یک رابط C انجام میشود: API C++ را در تابعهای extern "C" بپیچید و همانها را در سلام اعلان کنید. صدا زدن C از سلام روشش را نشان میدهد.
آنچه سلام کم دارد عمق است: کتابخانهی دستورهای SIMD همسطح C++ ندارد، مدل برنامهنویسی GPU ندارد، SDKهای سکوهای مختلف بسیار کمتری دارد و افراد بسیار کمی سامانههای بزرگ سلامی را به محیط عملیاتی رساندهاند.
سلام یا C++: کدام را انتخاب کنیم؟
C++ را انتخاب کنید وقتی:
- به یک کتابخانه یا موتور C++ موجود نیاز دارید (Unreal، Qt، یک پایگاه داده یا کتابخانهی ریاضی مشخص)؛
- پروژه بزرگ و بلندمدت است و به توسعهدهندگان زیادی نیاز دارید که از قبل این زبان را بلدند؛
- پختهترین بهینهسازها، پروفایلرها و پشتیبانی سکوها را لازم دارید، از جمله کنسولها، GPUها و هدفهای نهفتهی کمرایج؛
- طراحیتان برای محاسبه در زمان کامپایل به قالبها تکیه دارد.
سلام را انتخاب کنید وقتی:
- کارایی بومی در همان رده را با زبانی بسیار کوچکتر برای یادگیری و بازبینی میخواهید؛
- یک زنجیرهی ابزار با آزمون، قالببندی، بستهها و کامپایل متقابلِ داخلی برایتان از گستردگی اکوسیستم مهمتر است؛
- برنامه یک سرویس، ابزار خط فرمان یا جزئی از سیستم است که با کتابخانهی استاندارد و کتابخانههای C کارش راه میافتد؛
- گونههای جمع،
همخوانجامع و بررسیهای سختگیرانهی زمان کامپایل را بدون سازوکار قالبها میخواهید، یا کدی به فارسی.
پرسشهای پرتکرار
آیا زبان سلام جایگزین C++ است؟
برای برخی برنامهها بله: سرویسها، ابزارها و اجزایی که سرعت بومی لازم دارند اما به اکوسیستم کتابخانههای C++ نیاز ندارند. برای کدهای بزرگ C++ موجود، موتورهای بازی و پروژههایی که روی کتابخانههای C++ ساخته شدهاند، نه.
آیا سلام از C++ سریعتر است؟
در سنجش ۳۹ برنامهای پروژه، میانگین سلام ۱۰٫۶ میلیثانیه و میانگین C++ ۱۳٫۱ میلیثانیه بود، اما برنامهها کوتاهاند و زمان راهاندازی جزو هر اندازهگیری است. خلاصهی صادقانه این است که هر دو روی برنامههای کوچک محاسباتی در یک ردهاند.
آیا سلام RAII یا مخرب دارد؟
نه. ظرفهای هیپ صریحاً با آزادکن() آزاد میشوند، معمولاً با یک دیرکن درست بعد از ساختنشان. کامپایلر و salam memcheck به پیدا کردن اشتباهها کمک میکنند، اما آزاد کردن حافظه کار برنامهنویس است.
آیا سلام میتواند کتابخانههای C++ را صدا بزند؟
از راه یک رابط C. سلام تابعهای C اعلانشده در یک بلوک درونداد: را صدا میزند، پس یک کتابخانهی C++ به تابعهای پوششی extern "C" نیاز دارد؛ همان رویکردی که بیشتر زبانها به کار میبرند.