رفتن به محتوا

توابع و ارسال پارامتر

روش‌های مختلف ارسال آرگومان در ‎C++‎، بهینه‌سازی مقدار بازگشتی، سربارگذاری توابع، و ویژگی‌های مدرن مانند [[nodiscard]] را فرا بگیرید.

توابع: انتزاع اصلی

توابع واحد بنیادی سازمان‌دهی کد در ‎C++‎ هستند. برخلاف بسیاری از زبان‌های دیگر، ‎C++‎ کنترل دقیق بر نحوه ارسال آرگومان‌ها به توابع و بازگشت از آن‌ها به شما می‌دهد. این کنترل صرفاً قند نحوی نیست — پیامدهای عملکردی واقعی دارد.

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

مکانیزم‌های ارسال پارامتر

‎C++‎ چهار روش اصلی برای ارسال آرگومان پشتیبانی می‌کند. هر کدام مصالحه‌های متفاوتی از نظر عملکرد، ایمنی و معناشناسی دارند:

parameter_passing.cppc++23
#include <iostream>
#include <string>
#include <vector>

// 1. Pass by VALUE — the function gets a copy
//    Use for: small types (int, double, char), when you need a local copy
void square(int x) {           // x is a copy of the argument
    x = x * x;                 // Modifying x does NOT affect the caller
    std::cout << "Inside: " << x << '\n';
}

// 2. Pass by REFERENCE — the function works on the original
//    Use for: when you need to modify the caller's variable
void double_it(int& x) {       // x IS the caller's variable
    x *= 2;                    // This DOES modify the caller's variable
}

// 3. Pass by CONST REFERENCE — read-only access to the original, no copy
//    Use for: large objects you only need to read
void print_all(const std::vector<std::string>& names) {
    for (const auto& name : names) {
        std::cout << name << '\n';
    }
    // names.push_back("Eve");  // ERROR: names is const
}

// 4. Pass by POINTER — explicit nullable indirection
//    Use for: optional parameters (can be nullptr), C API interop
void maybe_increment(int* ptr) {
    if (ptr != nullptr) {       // Must check for null!
        ++(*ptr);
    }
}

int main() {
    int val = 5;

    square(val);
    std::cout << "After square: " << val << '\n';      // Still 5 (copied)

    double_it(val);
    std::cout << "After double_it: " << val << '\n';   // Now 10 (modified)

    std::vector<std::string> people = {"Alice", "Bob", "Charlie"};
    print_all(people);         // No copy of the vector — efficient

    maybe_increment(&val);     // Pass address explicitly
    std::cout << "After maybe_increment: " << val << '\n';  // 11

    maybe_increment(nullptr);  // Safe: the function checks for null
}
خروجی

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

بهترین شیوه

این راهنماها را از دستورالعمل‌های اصلی ‎C++‎ برای انتخاب نحوه ارسال پارامترها دنبال کنید:

- کپی ارزان و هرگز تغییر نمی‌کند؟ با مقدار ارسال کنید (int، double، char، bool، ساختارهای کوچک، std::string_view).
- کپی گران و فقط خوانده می‌شود؟ با const& ارسال کنید (std::string، std::vector، اشیاء بزرگ).
- نیاز به تغییر متغیر فراخوان‌کننده دارید؟ با & (ارجاع غیرثابت) ارسال کنید.
- ممکن است تهی باشد؟ با اشاره‌گر ارسال کنید. اما std::optional& را نیز در نظر بگیرید.
- تملک گرفتن (غرق کردن)؟ با مقدار ارسال کنید و جابه‌جا کنید (در ماژول معناشناسی انتقال پوشش داده می‌شود).

به عنوان قاعده سرانگشتی، اشیاء بزرگ‌تر از ۲ تا ۳ اشاره‌گر (۱۶ تا ۲۴ بایت در سیستم ۶۴ بیتی) باید با ارجاع ارسال شوند نه با مقدار.

  • انواع کوچک و ارزان را با مقدار ارسال کنید: int، double، char، bool، string_view
  • اشیاء بزرگ فقط‌خواندنی را با ارجاع ثابت ارسال کنید: const std::string&، const std::vector&
  • فقط وقتی تابع نیاز به تغییر شیء فراخوان‌کننده دارد با ارجاع غیرثابت ارسال کنید
  • وقتی nullptr آرگومان معتبری است با اشاره‌گر ارسال کنید — اما تا حد ممکن std::optional را ترجیح دهید
  • وقتی شک دارید، با ارجاع ثابت ارسال کنید — ایمن و کارآمد است

بهینه‌سازی مقدار بازگشتی (RVO/NRVO)

نگرانی رایج مبتدیان این است که بازگرداندن اشیاء بزرگ با مقدار به دلیل کپی گران است. در ‎C++‎ مدرن، این نگرانی تقریباً همیشه بی‌اساس است به لطف بهینه‌سازی مقدار بازگشتی (RVO) و بهینه‌سازی مقدار بازگشتی نام‌دار (NRVO).

RVO (تضمین‌شده از ‎C++‎17): وقتی تابع یک موقتی برمی‌گرداند (مثلاً return std::vector{1,2,3};)، کامپایلر باید شیء را مستقیماً در حافظه فراخوان‌کننده بسازد. هیچ کپی یا جابه‌جایی رخ نمی‌دهد.

NRVO (مجاز، نه تضمین‌شده): وقتی تابع یک متغیر محلی نام‌دار برمی‌گرداند (مثلاً return result;)، کامپایلر ممکن است آن متغیر را مستقیماً در حافظه فراخوان‌کننده بسازد. تمام کامپایلرهای اصلی این کار را در عمل انجام می‌دهند، حتی در حالت دیباگ.

توصیه عملی: آزادانه با مقدار بازگردانید. از پارامترهای خروجی (void f(std::vector& out)) برای اجتناب از کپی استفاده نکنید — بگذارید کامپایلر بازگشت را بهینه‌سازی کند.

سربارگذاری توابع و آرگومان‌های پیش‌فرض

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

overloading.cppc++23
#include <iostream>
#include <string>
#include <vector>

// Function overloading — same name, different parameter types/counts
void log(int value) {
    std::cout << "[INT] " << value << '\n';
}

void log(double value) {
    std::cout << "[DOUBLE] " << value << '\n';
}

void log(const std::string& value) {
    std::cout << "[STRING] " << value << '\n';
}

// Default arguments — provide fallback values
void greet(const std::string& name, const std::string& greeting = "Hello") {
    std::cout << greeting << ", " << name << "!\n";
}

// [[nodiscard]] (C++17) — warn if return value is ignored
[[nodiscard]] int compute(int a, int b) {
    return a * b + a;
}

// [[nodiscard]] with reason (C++20)
[[nodiscard("Ignoring error codes can hide failures")]]
int open_file(const std::string& path) {
    // Returns 0 on success, negative on error
    return 0;  // Simplified
}

// inline — suggests the compiler inline this function (also allows
// the function to be defined in a header without ODR violations)
inline int fast_max(int a, int b) {
    return (a > b) ? a : b;
}

int main() {
    log(42);               // Calls log(int)
    log(3.14);             // Calls log(double)
    log(std::string{"hi"});// Calls log(const string&)

    greet("Alice");                // Uses default: "Hello, Alice!"
    greet("Bob", "Good morning"); // Uses provided: "Good morning, Bob!"

    // int result = compute(3, 4);  // OK
    // compute(3, 4);               // WARNING: ignoring [[nodiscard]] return value

    int status = open_file("data.txt");  // OK: return value used
}
خروجی

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

دام

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

1. تبدیل‌های ضمنی ابهام ایجاد می‌کنند: اگر f(int) و f(double) دارید، فراخوانی f(3.14f) (یک float) مبهم است زیرا float به طور یکسان به هر دو int و double تبدیل می‌شود. راه‌حل: یک سربار f(float) اضافه کنید، یا از static_cast استفاده کنید.

2. آرگومان‌های پیش‌فرض با سربارها تداخل نامناسب دارند: void f(int x, int y = 0) و void f(int x) وقتی با یک آرگومان فراخوانی شوند ابهام ایجاد می‌کنند.

3. ارجاع ثابت در برابر مقدار: f(int) و f(const int&) برای آرگومان‌های lvalue مبهم هستند زیرا هر دو به طور یکسان تطابق خوبی دارند.

وقتی کامپایلر سربار مبهمی گزارش می‌دهد، حدس نزنید — پیام خطا را با دقت بخوانید. به شما می‌گوید کدام سربارها کاندید هستند و چرا به طور یکسان خوب هستند.

  • تبدیل‌های ضمنی می‌توانند تفکیک سربارگذاری را مبهم کنند — سربارهای صریح اضافه کنید تا حل شود
  • آرگومان‌های پیش‌فرض و سربارها می‌توانند تداخل کنند — از هر دو روی نام تابع یکسان اجتناب کنید
  • وقتی شک دارید، پیام خطای کامپایلر سربارهای کاندید و ابهام را لیست می‌کند
  • از static_cast برای انتخاب صریح یک سربار هنگام فراخوانی با آرگومان‌هایی که با چندین سربار تطابق دارند استفاده کنید
نکات کلیدی
  • انواع کوچک را با مقدار، انواع بزرگ فقط‌خواندنی را با ارجاع ثابت، و انواع قابل تغییر را با ارجاع غیرثابت ارسال کنید
  • آزادانه با مقدار بازگردانید — RVO/NRVO در عمل کپی را حذف می‌کند
  • سربارگذاری توابع بهترین تطابق را بر اساس انواع آرگومان انتخاب می‌کند — مراقب ابهام ناشی از تبدیل‌های ضمنی باشید
  • از [[nodiscard]] روی توابعی استفاده کنید که مقدار بازگشتی‌شان نباید نادیده گرفته شود (کدهای خطا، نتایج محاسباتی)
  • آرگومان‌های پیش‌فرض راحتی فراهم می‌کنند اما می‌توانند با سربارها تداخل کنند — از یکی یا دیگری استفاده کنید
  • inline اجازه می‌دهد توابع در هدرها تعریف شوند بدون نقض قاعده تعریف یکتا

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

(15 امتیاز)

1. چه زمانی باید `std::vector<int>` را با `const&` به جای مقدار ارسال کنید؟

2. بهینه‌سازی مقدار بازگشتی (RVO) چیست؟

3. ویژگی `[[nodiscard]]` چه کار می‌کند؟