رفتن به محتوا

راهنمای اصلی ‎C++‎ و الگوهای حرفه‌ای

راهنمای اصلی ‎C++‎ (‎C++‎ Core Guidelines)، قراردادهای نام‌گذاری، صحت const، اصول طراحی API، شیوه‌های مستندسازی، پروفایل عملکرد، و تکنیک‌های اشکال‌زدایی تولیدی را بیاموزید.

نوشتن کد ‎C++‎ حرفه‌ای

نوشتن ‎C++‎ صحیح و کارآمد ضروری است اما برای توسعه نرم‌افزار حرفه‌ای کافی نیست. ‎C++‎ حرفه‌ای همچنین به معنای کدی است که خوانا (قابل درک توسط دیگران و خود آینده‌تان)، قابل نگهداری (آسان برای تغییر بدون معرفی باگ)، و مقاوم (خطاها را با ظرافت مدیریت می‌کند و به‌صورت قابل پیش‌بینی شکست می‌خورد) باشد. راهنمای اصلی ‎C++‎ (‎C++‎ Core Guidelines) که توسط Bjarne Stroustrup و Herb Sutter نگهداری می‌شود، دهه‌ها حکمت سخت‌آموخته را در قوانین عملیاتی کدبندی می‌کند. این درس مؤثرترین راهنماها و شیوه‌های حرفه‌ای را پوشش می‌دهد.

مروری بر راهنمای اصلی ‎C++‎

راهنمای اصلی (Core Guidelines) مدیریت منابع، رابط‌ها، مدیریت خطا و نام‌گذاری را پوشش می‌دهد. در اینجا برخی از مهم‌ترین قوانین در عمل آمده است:

core_guidelines.cpp
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <span>
#include <stdexcept>

// R.1: Manage resources automatically using RAII
class DatabaseConnection {
public:
    explicit DatabaseConnection(const std::string& connection_string) {
        // Acquire resource in constructor
        std::cout << "Connected to: " << connection_string << "\n";
    }
    ~DatabaseConnection() {
        // Release resource in destructor — always runs
        std::cout << "Disconnected\n";
    }

    // Rule of 5: if you define a destructor, define or delete all 5
    DatabaseConnection(const DatabaseConnection&) = delete;
    DatabaseConnection& operator=(const DatabaseConnection&) = delete;
    DatabaseConnection(DatabaseConnection&&) noexcept = default;
    DatabaseConnection& operator=(DatabaseConnection&&) noexcept = default;

    void query(std::string_view sql) const {
        std::cout << "Executing: " << sql << "\n";
    }
};

// I.4: Make interfaces precisely and strongly typed
// BAD: void process(int type, void* data, int size);
// GOOD:
enum class DataType { json, xml, csv };

void process(DataType type, std::span<const std::byte> data) {
    // type-safe, no raw pointers, size is carried with the data
}

// F.3: Keep functions short and simple
[[nodiscard]] bool is_valid_email(std::string_view email) {
    auto at_pos = email.find('@');
    if (at_pos == std::string_view::npos) return false;
    auto dot_pos = email.find('.', at_pos);
    return dot_pos != std::string_view::npos && dot_pos > at_pos + 1;
}

int main() {
    // R.5: Prefer scoped objects — no unnecessary heap allocation
    DatabaseConnection db("host=localhost dbname=mydb");
    db.query("SELECT * FROM users");
    // db is automatically closed when it goes out of scope

    // R.11: Avoid raw new/delete — use smart pointers
    auto widget = std::make_unique<std::vector<int>>(100);

    std::cout << is_valid_email("user@example.com") << "\n";   // 1
    std::cout << is_valid_email("invalid-email") << "\n";       // 0

    return 0;
}

صحت const به‌عنوان یک نظم

صحت const فقط درباره جلوگیری از تغییر تصادفی نیست — نیت را مستند می‌کند، بهینه‌سازی‌های کامپایلر را ممکن می‌سازد، و کد را به‌صورت پیش‌فرض ایمن از نظر نخ می‌کند. همه چیز را const کنید مگر اینکه نیاز به تغییرپذیری داشته باشد:

const_correctness.cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

class StudentRegistry {
public:
    // const method — promises not to modify the object
    [[nodiscard]] size_t count() const { return students_.size(); }

    // const reference return — caller can read but not modify
    [[nodiscard]] const std::string& get_name(size_t index) const {
        if (index >= students_.size())
            throw std::out_of_range("Invalid student index");
        return students_[index];
    }

    // Non-const method — modifies the object
    void add_student(std::string name) {
        students_.push_back(std::move(name));
    }

    // const-correct algorithm usage
    [[nodiscard]] bool has_student(std::string_view name) const {
        return std::any_of(students_.cbegin(), students_.cend(),
            [name](const std::string& s) { return s == name; });
    }

private:
    std::vector<std::string> students_;
};

void print_registry(const StudentRegistry& reg) {
    // Can only call const methods on a const reference
    for (size_t i = 0; i < reg.count(); ++i) {
        std::cout << reg.get_name(i) << "\n";
    }
    // reg.add_student("Eve");  // ERROR: cannot call non-const method
}

int main() {
    StudentRegistry reg;
    reg.add_student("Alice");
    reg.add_student("Bob");

    print_registry(reg);

    // const local variables
    const auto count = reg.count();  // immutable
    const auto& name = reg.get_name(0);  // immutable reference

    std::cout << "Count: " << count << ", First: " << name << "\n";

    return 0;
}

اصول طراحی API

قانون طلایی طراحی API در ‎C++‎، منسوب به Scott Meyers: رابط‌ها را آسان برای استفاده صحیح و دشوار برای استفاده نادرست طراحی کنید.

از انواع قوی استفاده کنید: به‌جای void set_timeout(int ms) از void set_timeout(std::chrono::milliseconds timeout) استفاده کنید. فراخواننده نمی‌تواند به‌طور تصادفی ثانیه بفرستد وقتی شما میلی‌ثانیه انتظار دارید.

از [[nodiscard]] استفاده کنید: هر تابعی که کد خطا، منبع یا مقدار محاسبه‌شده برمی‌گرداند باید [[nodiscard]] باشد. نادیده‌گرفتن مقدار بازگشتی تقریباً همیشه باگ است.

معنابخشی مقداری را ترجیح دهید: انواع کوچک را به‌صورت مقدار و انواع بزرگ‌تر را به‌صورت const& ارسال کنید. به‌صورت مقدار برگردانید (کامپایلر حذف کپی اعمال می‌کند). از پارامترهای خروجی اجتناب کنید.

سطح عمومی را حداقل کنید: هر عضو عمومی تعهدی است که باید حفظ شود. به‌صورت پیش‌فرض از private استفاده کنید و فقط آنچه ضروری است را نمایان کنید.

اصل کمترین شگفتی را دنبال کنید: توابع باید آنچه نامشان دلالت می‌کند انجام دهند، نه بیشتر. اثرات جانبی باید از نام آشکار باشند (save_to_file، نه process).

قراردادهای نام‌گذاری و مستندسازی

نام‌گذاری یکنواخت کد را در سطح تیم خوانا می‌کند. قراردادهای رایج ‎C++‎:

انواع: PascalCase — class HttpClient، struct Point3D، enum class Color
توابع/متدها: snake_case یا camelCase — calculate_total()، getSize()
متغیرها: snake_case — int item_count، double total_price
متغیرهای عضو: زیرخط انتهایی — int count_، std::string name_
ثوابت: پیشوند k یا UPPER_SNAKE — constexpr int kMaxRetries = 3
فضاهای نام: lowercase — namespace network، namespace detail

یک قرارداد انتخاب کنید و آن را در سطح پروژه اعمال کنید (clang-format + clang-tidy می‌توانند این را خودکار کنند).

مستندسازی: از نظرات به سبک Doxygen برای APIهای عمومی استفاده کنید:

/// @brief Connects to the database.
/// @param connection_string The database connection URI.
/// @return true if connection succeeded.
/// @throws DatabaseError if the connection fails.
[[nodiscard]] bool connect(std::string_view connection_string);

پروفایل عملکرد

هرگز بدون پروفایل بهینه‌سازی نکنید. این ابزارها به شما می‌گویند زمان واقعاً کجا صرف می‌شود و حدس را با داده جایگزین می‌کنند:

benchmark.cpp
# Linux: perf — hardware performance counters
perf record -g ./my_program         # record call graph
perf report                           # interactive flamegraph viewer

# Linux: perf stat — high-level statistics
perf stat ./my_program
# Output: cycles, instructions, cache misses, branch misses, IPC

# macOS: Instruments (via Xcode)
xcrun xctrace record --template 'Time Profiler' --launch ./my_program

# Cross-platform: Google Benchmark (micro-benchmarking)
# In your code:
#include <benchmark/benchmark.h>

static void BM_VectorPushBack(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        for (int i = 0; i < state.range(0); ++i) {
            v.push_back(i);
        }
        benchmark::DoNotOptimize(v.data());
    }
}
BENCHMARK(BM_VectorPushBack)->Range(8, 1 << 16);

static void BM_VectorReserved(benchmark::State& state) {
    for (auto _ : state) {
        std::vector<int> v;
        v.reserve(state.range(0));
        for (int i = 0; i < state.range(0); ++i) {
            v.push_back(i);
        }
        benchmark::DoNotOptimize(v.data());
    }
}
BENCHMARK(BM_VectorReserved)->Range(8, 1 << 16);

BENCHMARK_MAIN();
بهترین شیوه

هنگام بازبینی کد ‎C++‎، این مسائل رایج را بررسی کنید:

1. مدیریت منابع: آیا هر منبع (حافظه، فایل‌ها، قفل‌ها) توسط RAII مدیریت می‌شود؟ آیا new/delete خام وجود دارد؟
2. صحت const: آیا پارامترها، توابع عضو و متغیرهای محلی تا حد ممکن const هستند؟
3. مدیریت خطا: آیا مقادیر بازگشتی بررسی می‌شوند؟ آیا استثناها در سطوح مناسب گرفته می‌شوند؟ آیا std::expected یا کدهای خطا به‌صورت یکنواخت استفاده می‌شوند؟
4. ایمنی نخ: آیا منابع مشترک محافظت می‌شوند؟ آیا مسابقه داده ممکن است؟ آیا سنیتایزر مشکلات را می‌گیرد؟
5. معنابخشی انتقال: آیا اشیا به‌جای کپی، جایی که مناسب است منتقل (move) می‌شوند؟ آیا فراخوانی‌های std::move بر روی اشیای const وجود دارد (که بی‌صدا کپی می‌کنند)؟
6. وضوح API: آیا نام توابع توصیفی هستند؟ آیا پارامترها نوع قوی دارند؟ آیا [[nodiscard]] بر روی مقادیر بازگشتی مهم استفاده شده؟
7. هشدارهای ساخت: آیا کد با -Wall -Wextra -Wpedantic بدون هشدار کامپایل می‌شود؟ آیا تمام هشدارها رسیدگی شده‌اند، نه سرکوب شده‌اند؟
8. تست: آیا حالات لبه‌ای تست شده‌اند؟ آیا مسیرهای کد جدید تحت پوشش تست هستند؟ آیا سنیتایزرها موفق می‌شوند؟

دام

اشکال‌زدایی ‎C++‎ در محیط تولیدی سخت‌تر از توسعه است. برای آن آماده شوید:

همیشه با نمادهای اشکال‌زدایی ارسال کنید: باینری‌های تولیدی را با RelWithDebInfo بسازید. نمادهای اشکال‌زدایی در یک فایل .debug جداگانه هیچ سربار زمان اجرایی اضافه نمی‌کنند اما تحلیل خرابی را ممکن می‌سازند.

داده‌های ساختاریافته ثبت کنید: از ثبت‌وقایع ساختاریافته (JSON) با سطوح ثبت‌وقایع استفاده کنید. زمان‌نشان‌ها، شناسه‌های نخ و شناسه‌های درخواست را شامل کنید. از spdlog یا std::format برای ثبت‌وقایع سریع و ایمن از نظر نوع استفاده کنید.

تخلیه هسته (Core Dumps): تخلیه هسته را فعال کنید (ulimit -c unlimited) و بدانید چگونه آن‌ها را با gdb تحلیل کنید. سرورهای نماد، اطلاعات اشکال‌زدایی را برای باینری‌های مستقرشده ذخیره می‌کنند.

باگ‌های رایج تولیدی:
- نشت حافظه — از ASan در محیط‌های آماده‌سازی یا ابزارهایی مانند Valgrind / Heaptrack استفاده کنید
- بن‌بست (Deadlock) — از TSan در تست استفاده کنید، ترتیب گرفتن قفل را ثبت کنید، زمان‌بندی قفل تعیین کنید
- افت عملکرد — محک‌زنی مداوم در CI افت عملکرد را قبل از استقرار تشخیص می‌دهد
- سازگاری ABI — تغییر چیدمان ساختارها یا جداول توابع مجازی ABI را خراب می‌کند؛ از الگوی Pimpl برای ABIهای پایدار استفاده کنید

نکات کلیدی
  • راهنمای اصلی ‎C++‎ را دنبال کنید — RAII برای منابع، قانون ۵، نوع‌گذاری قوی، APIهای عمومی حداقلی
  • همه چیز را به‌صورت پیش‌فرض const کنید — نیت را مستند می‌کند، از باگ‌ها جلوگیری می‌کند، و بهینه‌سازی‌ها را ممکن می‌سازد
  • APIها را طوری طراحی کنید که استفاده صحیح آسان و استفاده نادرست دشوار باشد — انواع قوی، [[nodiscard]]، معنابخشی مقداری
  • قبل از بهینه‌سازی پروفایل کنید — از perf، Instruments یا Google Benchmark برای یافتن گلوگاه‌های واقعی استفاده کنید
  • کد را از نظر مدیریت منابع، صحت const، ایمنی نخ و مدیریت صحیح خطا بازبینی کنید
  • برای اشکال‌زدایی تولیدی آماده شوید: با نمادهای اشکال‌زدایی ارسال کنید، از ثبت‌وقایع ساختاریافته استفاده کنید، تخلیه هسته را فعال کنید

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

(15 امتیاز)

1. قانون ۵ در راهنمای اصلی ‎C++‎ چه می‌گوید؟

2. قانون طلایی طراحی API در ‎C++‎ چیست؟

3. چرا باید قبل از بهینه‌سازی پروفایل کنید؟