رفتن به محتوا

کنترل جریان و حلقه‌ها

بر تمام ساختارهای کنترل جریان ‎C++‎ شامل if/else، switch، تمام اشکال حلقه، حلقه for مبتنی بر محدوده، مقداردهی‌کننده‌های ‎C++‎17 و اتصالات ساختاریافته مسلط شوید.

کنترل جریان در ‎C++‎

دستورات کنترل جریان ترتیب اجرای برنامه شما را تعیین می‌کنند. ‎C++‎ مجموعه غنی از ساختارهای کنترل جریان ارائه می‌دهد که بسیار فراتر از مبانی به ارث رسیده از C است. ‎C++‎ مدرن (‎C++‎17 و بعد) ویژگی‌هایی مانند if با مقداردهی‌کننده و constexpr if اضافه می‌کند که کنترل جریان را بیانگرتر و ایمن‌تر می‌سازند.

یک اصل کلیدی: دامنه متغیرها را حداقل کنید. هر متغیر باید تا حد ممکن نزدیک به اولین استفاده‌اش تعریف شود و در باریک‌ترین دامنه ممکن. ویژگی‌های ‎C++‎17 مانند if با مقداردهی‌کننده به شما کمک می‌کنند این اصل را رعایت کنید.

if/else و switch

دستورات if و switch مکانیزم‌های اصلی انشعاب هستند. ‎C++‎17 بندهای مقداردهی‌کننده به هر دو اضافه می‌کند:

control_flow.cppc++23
#include <iostream>
#include <map>
#include <string>
#include <optional>

std::optional<int> find_value(const std::string& key) {
    std::map<std::string, int> data = {{"alpha", 1}, {"beta", 2}, {"gamma", 3}};
    if (auto it = data.find(key); it != data.end()) {  // C++17 if with initializer
        return it->second;
    }
    return std::nullopt;
}

int main() {
    // C++17: if with initializer — the variable's scope is limited to the if/else
    if (auto val = find_value("beta"); val.has_value()) {
        std::cout << "Found: " << *val << '\n';
    } else {
        std::cout << "Not found\n";
    }
    // 'val' is no longer in scope here — cleaner!

    // switch with C++17 initializer
    enum class Color { Red, Green, Blue };
    Color c = Color::Green;

    switch (auto name = "unknown"; c) {  // C++17 switch with initializer
        case Color::Red:   name = "red";   std::cout << name << '\n'; break;
        case Color::Green: name = "green"; std::cout << name << '\n'; break;
        case Color::Blue:  name = "blue";  std::cout << name << '\n'; break;
    }

    // [[fallthrough]] attribute (C++17) — document intentional fallthrough
    int priority = 2;
    switch (priority) {
        case 1:
            std::cout << "Critical: ";
            [[fallthrough]];       // Tells the compiler this is intentional
        case 2:
            std::cout << "Important: ";
            [[fallthrough]];
        case 3:
            std::cout << "Handle it\n";
            break;
        default:
            std::cout << "Unknown priority\n";
    }
}
خروجی

برای کامپایل و اجرای این نمونه، روی اجرا بزنید.

حلقه‌ها: for، while، do-while و حلقه for مبتنی بر محدوده

‎C++‎ اشکال متعدد حلقه ارائه می‌دهد. حلقه for مبتنی بر محدوده (از ‎C++‎11) هنگام پیمایش کانتینرها ترجیح داده می‌شود زیرا خطاهای یک‌واحدی را حذف و وضوح را بیشتر می‌کند:

loops.cppc++23
#include <iostream>
#include <vector>
#include <string>
#include <map>

int main() {
    std::vector<std::string> names = {"Alice", "Bob", "Charlie"};

    // Traditional for loop — use when you need the index
    for (std::size_t i = 0; i < names.size(); ++i) {
        std::cout << i << ": " << names[i] << '\n';
    }

    // Range-based for loop (C++11) — preferred for iteration
    for (const auto& name : names) {        // const auto& avoids copying
        std::cout << name << '\n';
    }

    // Range-based for with structured bindings (C++17)
    std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}, {"Charlie", 92}};
    for (const auto& [name, score] : scores) {   // Destructure the pair
        std::cout << name << " scored " << score << '\n';
    }

    // C++20: range-based for with init-statement
    for (auto v = std::vector{1, 2, 3, 4, 5}; auto elem : v) {
        std::cout << elem << ' ';
    }
    std::cout << '\n';

    // while loop — use when the number of iterations is unknown
    int n = 1;
    while (n <= 1024) {
        std::cout << n << ' ';
        n *= 2;
    }
    std::cout << '\n';

    // do-while — body executes at least once
    int input;
    do {
        std::cout << "Enter a positive number: ";
        input = 42;  // Simulated input for example
    } while (input <= 0);
}
خروجی

برای کامپایل و اجرای این نمونه، روی اجرا بزنید.

اتصالات ساختاریافته (Structured Bindings) در ‎C++‎17

اتصالات ساختاریافته (Structured Bindings) در ‎C++‎17 به شما امکان می‌دهند یک شیء را به اجزای تشکیل‌دهنده‌اش تجزیه کنید. آن‌ها با آرایه‌ها، ساختارها با تمام اعضای عمومی، std::pair، std::tuple و هر نوعی که پروتکل اتصالات ساختاریافته را پشتیبانی کند کار می‌کنند.

نحو auto [a, b] = expr; ارجاعات نام‌دار به اجزای expr ایجاد می‌کند. می‌توانید تعیین‌کننده‌های const و & اضافه کنید: const auto& [a, b] = expr; ارجاعات ثابت بدون کپی ایجاد می‌کند.

اتصالات ساختاریافته به‌ویژه با پیمایش std::map (تجزیه std::pair)، بازگشت‌های تابع که از std::pair یا std::tuple استفاده می‌کنند، و مقدار بازگشتی متد insert روی کانتینرهای انجمنی قدرتمند هستند.

بهترین شیوه

از break برای خروج زودهنگام از حلقه وقتی آنچه نیاز دارید پیدا کردید استفاده کنید. از continue برای رد کردن بقیه تکرار فعلی و رفتن به تکرار بعدی استفاده کنید. هر دو وقتی با احتیاط استفاده شوند درک حلقه‌ها را آسان‌تر می‌کنند.

حلقه for مبتنی بر محدوده را بر حلقه مبتنی بر اندیس ترجیح دهید وقتی به اندیس نیاز ندارید. این دسته‌های کاملی از باگ‌ها را حذف می‌کند: خطاهای یک‌واحدی، استفاده از متغیر اشتباه به عنوان اندیس، و هشدارهای مقایسه علامت‌دار/بدون‌علامت.

وقتی به اندیس نیاز دارید، استفاده از std::size_t به عنوان نوع متغیر حلقه را در نظر بگیرید تا با نوع بازگشتی .size() مطابقت داشته باشد، یا از std::ssize() (در ‎C++‎20) استفاده کنید که اندازه علامت‌دار برمی‌گرداند.

از حلقه‌های عمیقاً تو در تو اجتناب کنید — اگر سه یا بیشتر حلقه تو در تو دارید، حلقه‌های داخلی را به توابع جداگانه استخراج کنید. این خوانایی و قابلیت آزمون را بهبود می‌بخشد.

  • حلقه‌های for مبتنی بر محدوده را بر حلقه‌های مبتنی بر اندیس ترجیح دهید وقتی اندیس لازم نیست
  • از const auto& در حلقه for مبتنی بر محدوده برای جلوگیری از کپی‌های غیرضروری استفاده کنید
  • از اتصالات ساختاریافته برای تجزیه جفت‌ها و تاپل‌ها در متغیرهای حلقه استفاده کنید
  • حلقه‌های عمیقاً تو در تو را به توابع نام‌دار استخراج کنید برای وضوح بیشتر

constexpr if در ‎C++‎17: انشعاب زمان کامپایل

if constexpr شرط را در زمان کامپایل ارزیابی می‌کند و شاخه‌ای که انتخاب نشده را کنار می‌گذارد. این در برنامه‌نویسی قالب ضروری است جایی که شاخه‌های مختلف ممکن است برای همه انواع حتی معتبر نباشند:

constexpr_if.cppc++23
#include <iostream>
#include <type_traits>
#include <string>

template <typename T>
void describe(T value) {
    std::cout << "Value: " << value;

    if constexpr (std::is_integral_v<T>) {
        // This branch only compiles when T is an integer type
        std::cout << " (integer, " << (value % 2 == 0 ? "even" : "odd") << ")";
    } else if constexpr (std::is_floating_point_v<T>) {
        // This branch only compiles when T is a floating-point type
        std::cout << " (floating-point)";
    } else {
        std::cout << " (other type)";
    }
    std::cout << '\n';
}

int main() {
    describe(42);           // Value: 42 (integer, even)
    describe(3.14);         // Value: 3.14 (floating-point)
    describe("hello");      // Value: hello (other type)
}
خروجی

برای کامپایل و اجرای این نمونه، روی اجرا بزنید.

نکات کلیدی
  • if/switch با مقداردهی‌کننده در ‎C++‎17 دامنه متغیر را به دستور محدود می‌کنند — از آن‌ها برای تمیز نگه داشتن کد استفاده کنید
  • حلقه‌های for مبتنی بر محدوده خطاهای یک‌واحدی و مشکلات مقایسه علامت‌دار/بدون‌علامت را حذف می‌کنند
  • اتصالات ساختاریافته (‎C++‎17) جفت‌ها، تاپل‌ها و ساختارها را به متغیرهای نام‌دار تجزیه می‌کنند
  • از [[fallthrough]] برای مستندسازی عبور عمدی بین موارد switch استفاده کنید
  • if constexpr در ‎C++‎17 شرط‌ها را در زمان کامپایل ارزیابی می‌کند — شاخه کنار گذاشته‌شده کامپایل نمی‌شود
  • دامنه متغیرها را حداقل کنید: متغیرها را تا حد ممکن نزدیک به اولین استفاده تعریف کنید

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

(15 امتیاز)

1. مزیت `if` با مقداردهی‌کننده ‎C++‎17 چیست: `if (auto x = f(); x > 0)`؟

2. `for (const auto& [key, val] : my_map)` چه کار می‌کند؟

3. با شاخه کنار گذاشته‌شده یک دستور `if constexpr` چه اتفاقی می‌افتد؟