
چطور کد C++ را ایمن و سریعتر از C بنویسیم؛ قواعد یک مهندس با ۲۰ سال سابقه
یک مهندس نرمافزار با تجربه در سیستمهای نهفته، قواعد عملی را گردآوری کرده که رعایت آنها کد C++ را نهتنها ایمنتر، که در بعضی موارد سریعتر از معادل C میکند. این قواعد حول کنترل حافظه و مدیریت خطا
نکات کلیدی
- بین قواعد سبکی (برای خوانایی انسان) و قواعد عملکردی (برای جلوگیری از شکست برنامه) تفاوت بنیادی وجود دارد.
- یکسانسازی نوع عددی در کل پشته و حذف تبدیلهای غیرضروری، هم باگها را کم میکند و هم میتواند کارایی را بالا ببرد.
- مدیریت خطا با std::expected و استفاده از [[nodiscard]] بررسی نتیجه را اجباری میکند و احتمال نادیدهگرفتن خطا را — بهخصوص با تنظیم اخطارها بهعنوان خطا — در زمان کامپایل از بین میبرد.
اگر فکر میکنید C++ همیشه کندتر و پیچیدهتر از C است، شاید وقتش رسیده این باور را دوباره بسنجید. یک مهندس باتجربه در حوزهی سیستمهای نهفته و بحرانی — کسی که روی مودمهای نرمافزاری برای میکروکنترلرهای Cortex-M کار کرده — مجموعهای از قواعد انضباطی را به اشتراک گذاشته که نهتنها خطاهای رایج را کم میکند، بلکه ادعا دارد کد نهایی را در عمل حتی از معادل نوشتهشده به C هم سریعتر میکند. این ادعا بر یک محدودیت سخت تکیه دارد: «کل هسته باید روی یک میکروکنترلر نهفته کامپایل شود، نه فقط روی یک ایستگاه کاری قدرتمند».
این قواعد حاصل دو دهه کار تیمی، بازبینیهای مداوم و تجربه در پروژههای مختلف است. نویسنده تاکید میکند این فهرست فرمان خشک نیست؛ خلاصهی درسهایی است که از کار در محیطهای با محدودیتهای سخت گرفته است. کلید موفقیت، یکدستبودن اجرای انضباط در همهی پروژههاست. دو نتیجهی مهم این کار: اول، از همان بار اول کد درستتری مینویسید و فاصلهی «کار میکند» تا «درست است» کم میشود. دوم، استفادهی مجدد از کد ارزانتر میشود؛ تابعی که هشت ماه پیش نوشتهاید، با دردسر خیلی کمتر در پروژهی جدید جا میگیرد، چون همان نوعها را بهکار میبرد، خطاها را به یک شکل برمیگرداند و مالک چیزی که نباید، نیست.
تفاوت حیاتی: سبک در برابر انضباط عملکردی
قبل از هر چیز، یک تمایز اساسی وجود دارد که از خود قواعد هم مهمتر است: دو نوع «استاندارد کدنویسی» داریم که هر کدام اگر رعایت نشوند، به شکل متفاوتی مشکل میسازند.
• سبک (Style): مربوط به خوانایی برای انسان است؛ مثل نامگذاری متغیرها، کلاسها و توابع، نحوهی فرمت، تب یا فاصله، و شکستن خطوط. یک فرمتر مثل clang-format یا astyle میتواند بیشتر این قواعد را اعمال کند. اگر سبک رعایت نشود، چیزی نمیشکند؛ فقط نویز بصری برای نگهدارندهی بعدی بهجا میماند. مهم است، اما اهمیتش برای انسانهاست، نه کامپایلر.
• انضباط عملکردی (Functional Discipline): اینجا راهنمای هسته C++ (C++ Core Guidelines) ارزش خود را نشان میدهد: استفادهی حسابشده از cast، نوعهای سختگیرانه، مدیریت روشن خطا، اصل DRY (تکرار نکن)، استفادهی گسترده از const، بافرهایی که طول خود را در نوعشان حمل میکنند (مثل std::array بهجای اشارهگر خام و یک اندازهی جدا)، محدودههای کوچک، و مقداردهی اولیهی هر شیء قبل از استفاده. اگر اینها را رعایت نکنید، برنامه واقعا میتواند زمین بخورد: یک تبدیل بیصدا، خواندن از متغیر مقداردهینشده، ارجاع آویزان، یا بافری که طولش را گم کرده.
نویسنده تقریبا همهی این دستورالعملها را اجرا میکند. «تقریبا»، چون هدف نهفته سه استثنای عملکردی را تحمیل میکند: بدون استثنا (no exceptions)، بدون RTTI و بدون تخصیص پویا در مسیر پردازش سیگنال. بقیهی دستورالعملها پابرجا هستند. بنابراین، کامپایل با فلگهای -fno-exceptions و -fno-rtti و با اخطارها بهعنوان خطا انجام میشود.
چهار عادت کلیدی برای کد ایمن و کارا
با در نظر گرفتن محدودیتهای نهفته، قواعد عملی به چهار عادت اصلی سازماندهی میشوند.
۱) حافظه و تغییرپذیری، تحت قفل و کلید
- بدون هیپ (Heap) در مسیر بحرانی: هر بافر یک std::array ثابت است که اندازهاش با یک ثابت زمان کامپایل مشخص میشود و نوع و طولش با هم جابهجا میشوند. گیرنده در حین اجرا چیزی تخصیص نمیدهد؛ استخرها (pool) قبل از شروع جریان داده آماده میشوند و فراخوانیهای بلادرنگ (real-time callbacks) هرگز تخصیص حافظه نمیدهند، مسدود نمیشوند و به I/O دسترسی ندارند.
- پشتهی کراندار و شناختهشده؛ بدون بازگشت بیکران: با حذف هیپ، پشته آخرین جایی است که حافظه میتواند غافلگیرتان کند. پس بازگشت (recursion) در مسیر سیگنال ممنوع است و عمق آن باید محدود و روشن باشد. با فلگ -fstack-usage کامپایل میشود و هر تابع در هسته یک قاب ثابت زمان کامپایل را گزارش میکند. هیچ آرایهی با طول متغیر یا alloca وجود ندارد؛ بنابراین چیزی در زمان اجرا پشته را بزرگ نمیکند.
- const بهصورت پیشفرض؛ ارجاع بهجای اشارهگر: دادهها const هستند مگر اینکه دلیلی برای تغییر داشته باشند و هرجا اشارهگر خام کافی بود، از مرجع استفاده میشود. هرچه بخشهای کمتری قابل تغییر باشند، احتمال خطا کمتر میشود.
۲) یک نوع، از بالا تا پایین
- یک نوع عددی انتخابشده در زمان کامپایل: کل مودم روی یک نوع اسکالر واحد به نام Real پارامتریزه شده که بهطور پیشفرض float است. این نوع از همان ابتدا دقت بیشتری از آنچه دادهها عملا دارند، فراهم میکند. تایید با بردارهای طلایی دقیق بیتی (bit-exact golden vectors) و یک رمزگشای مرجع مستقل انجام میشود، نه با ساختی «دقیقتر».
- تقریبا بدون تبدیل نوع (cast): یکدستکردن پشتهی فراخوانی روی یک نوع، باعث میشود توابع برای صدا زدن همدیگر دست به تبدیل نزنند. تبدیلها پناهگاه باگها هستند و جایی که کامپایلر دیگر کمکی نمیکند. تنها تبدیل مجاز در مرز APIهایی است که مالکشان نیستید؛ مثلا بافر int16 یک کدک.
- نوعهای قوی، نه اعداد لخت: هر کمیت فیزیکی واحدش را در نوع خودش دارد: Frequency، Angle، Decibel. نمیتوانید عددی را بسازید یا بخوانید بدون اینکه واحدش را نام ببرید؛ پس یک هرتز با یک کیلوهرتز اشتباه نمیشود. این پوششها هزینهی زمان اجرا ندارند و در نهایت به همان عدد پایهای کامپایل میشوند.
۳) بررسیشده، نه امیدوارانه
- خطاها «مقدار» هستند، نه سورپرایز: با غیرفعالبودن استثناها، یک عملیات خطاپذیر یک [[nodiscard]] std::expected<T, Status> برمیگرداند. معنایش ساده است: «یا یک مقدار، یا دلیل نبودنش». شما باید آن را رسیدگی کنید؛ نمیتوانید نادیدهاش بگیرید.
- یک راه برای شکست، در همهجا: اگر تابعی میتواند شکست بخورد، همان نوع خطاپذیر واحد را برمیگرداند (یک Result یا Status). هرگز یک bool ساده که یواشکی یعنی «آیا کار کرد؟» برنگردانید. داشتن یک مسیر واحد یعنی کسی مجبور نیست برای هر تابع قرارداد جدیدی را حفظ کند.
- محاسبه و بررسی در زمان کامپایل: هر چیزی که قبل از اجرای برنامه قابل دانستن است — یک جدول، یک اندازه، یک هندسه — constexpr است. ناورداییهایی که باید برقرار بمانند با static_assert بررسی میشوند؛ پس یک فرض اشتباه باعث شکست ساخت میشود، نه شکست ماموریت.
۴) کمتر مجبور به اعتماد باشید
- بدون ماکرو برای مقادیر یا منطق: یک ثابت constexpr است، مجموعهای از حالتها enum class است، یک تابع کوچک کمکی inline یا template است. #define فقط جایگزینی متنی بینوع و بیمحدوده است.
- بدون حالت تغییرپذیر مشترک در هسته: الگوریتمهای قابلحمل عمدا تکریسهایاند. در یک قطعهی کوچک، قفل (mutex) گران است و دلیل محکمی برای چندریسهایبودن وجود ندارد.
- هسته قابلحمل، پلتفرم در لبهها: الگوریتمها از UI، سیستمعامل یا درایورها چیزی نمیدانند. همان منبعی که یک ابزار تشخیصی دسکتاپ را اجرا میکند، داخل فرمور هم کامپایل میشود.
چرا این قواعد میتواند به کارایی بهتر برسد؟
- حذف هزینههای پنهان تبدیل نوع: وقتی کل پشتهی فراخوانی روی یک نوع واحد (مثلا float) کار میکند، تبدیل نوع بین لایهها حذف میشود. یک تبدیل int به float یا ارتقای بیدلیل به double روی میکروکنترلری بدون سختافزار double میتواند به فراخوانی نرمافزاری پرهزینه تبدیل شود. چند مورد از این، ضرب در حلقههای بلادرنگ، میتواند بودجهی سیکلی یک حلقهی DSP را بخورد و ضربالاجل را از بین ببرد.
- استفادهی کامل از سختافزار: با اطمینان از اینکه محاسبات در نوعی انجام میشود که سختافزار بهطور بومی از آن پشتیبانی میکند (مثلا float32 روی FPU مبتنی بر ARM)، کد به دستورالعملهای بهینهشدهای مثل VMUL و VMLA تبدیل میشود.
- پیشبینیپذیری و حذف مکانیزمهای سنگین: حذف تخصیص پویا در مسیر بحرانی، نهتنها شکست تخصیص و قطعهقطعهشدن هیپ را حذف میکند، بلکه تاخیرهای غیرقطعی (nondeterministic latency) را هم برمیدارد؛ چیزی که میتواند ضربالاجلهای بلادرنگ را نقض کند.
اجرا به «اعتماد صرف» واگذار نمیشود
رعایت این قواعد صرفا بر عهدهی برنامهنویس نیست. یک فرمتر مثل clang-format نیمی از قواعد سبکی را اعمال میکند. clang-tidy با فعالکردن چکهای cppcoreguidelines، bugprone، performance و modernize — و تنظیم استاندارد C++23 بهعنوان هدف پروژه — بخش بزرگی از قواعد عملکردی را enforce میکند. یک بررسیکنندهی پیچیدگی مثل lizard هم از رشد آرام توابع و غیرقابلنگهداریشدن آنها جلوگیری میکند. در این پروژه، میانگین پیچیدگی سیکلوماتیک توابع زیر ۶ است.
این قواعد که در کورهی پروژههای نهفته و بحرانی شکل گرفتهاند، یادآوری میکنند انضباط در کدنویسی — وقتی درست و یکدست در همهجا اعمال شود — فقط مانع شکست نیست؛ میتواند مسیر روشنتری بهسوی کارایی بالاتر هم باشد. بحث صرفا سلیقه یا سبک نیست؛ موضوع، ساختن سیستمهایی است که در سختترین شرایط هم بتوان به آنها اعتماد کرد.
ابزارهای مرتبط والور
سوالات متداول
چطور C++ میتواند سریعتر از C باشد؟
با حذف هزینههای پنهان مثل تبدیلهای نوع غیرضروری که میتوانند به فراخوانیهای نرمافزاری پرهزینه تبدیل شوند، یکسانسازی نوع عددی در کل پشته که به کامپایلر اجازهی بهینهسازی بهتر میدهد، و حذف مکانیزمهای سنگین مثل تخصیص پویا در مسیرهای بحرانی که تاخیر غیرقطعی ایجاد میکنند.
چرا در سیستمهای نهفته از استثنا (exception) استفاده نمیکنند؟
به دو دلیل اصلی: اول، هزینهی اجرایی و اثرات جانبی استثناها برای منابع محدود میکروکنترلرها قابلتوجه و غیرقطعی است. دوم، در سیستمهای بحرانی بلادرنگ، پیشبینیپذیری رفتار اهمیت دارد و استثناها با تغییر ناگهانی مسیر اجرا میتوانند این پیشبینیپذیری را نقض کنند.
[[nodiscard]] در C++ چه میکند؟
این ویژگی به کامپایلر میگوید اگر مقدار بازگشتی تابع نادیده گرفته شود، هشدار بدهد (و اگر اخطارها را بهعنوان خطا تنظیم کرده باشید، ساخت را متوقف کند). این کار بررسی نتیجهی توابع خطاپذیر را اجباری میکند.
منابع
- The rules I build by — DEV Community
این مقاله با کمک هوش مصنوعی از منابع بالا تدوین و توسط امیرعلی کریمی بازبینی شده است. قوانین محتوایی والور

بنیانگذار و سردبیر والور
امیرعلی کریمی بنیانگذار و سردبیر والور است. مقالههای والور با کمک هوش مصنوعی و بر پایهی چند منبع معتبر بینالمللی تدوین میشوند و من پیش از انتشار، هر مطلب را از نظر درستی اطلاعات، روانی متن و منابع بازبینی و تایید میکنم. منابع هر مقاله در انتهای همان صفحه فهرست شدهاند.
مطالب مرتبط
معماری توسعه نرمافزار در عصر هوش مصنوعی: از کدنویس تا مهندس زمینه
آیندهی برنامهنویسی دیگر دربارهی تعداد خطوط کد نیست. با ظهور مدلهای «سیستم ۱» و پروتکل MCP، نقش توسعهدهنده از کدنویس به معمار سیستمهای خودگردان هوش مصنوعی در حال تغییر است.
جدال سرعت در هوش مصنوعی؛ از درخواست ترمز تا اختلاف نظرهای ایمنی
مدیران شرکتهای پیشروی هوش مصنوعی میگویند سرعت توسعه باید کنترلشده باشد؛ در مقابل، برخی رهبران صنعت راهحل را در مهندسی ایمنی و خودنظارتی میبینند. این شکاف اخیرا پررنگتر شده است.
وقتی هوشمصنوعی کد مینویسد: چطور تغییرات مخرب در API را شناسایی کنیم؟
ابزارهای هوشمصنوعی مثل GitHub Copilot و Cursor گاهی با اضافه کردن پارامترهای اختیاری یا عمومیکردن توابع، قراردادهای نرمافزار را عوض میکنند. این مقاله یک روش عملی و اسکریپت پایتون برای پیدا کردن