رفتن به محتوا

ارسال کامل و std::forward

درک کنید که چرا ارجاع‌های rvalue نام‌دار lvalue هستند، الگوی استاندارد ارسال با std::forward را بیاموزید و ببینید چگونه emplace_back از ارسال متغیر (variadic forwarding) برای ساخت اشیاء در محل استفاده می‌کند.

مشکل: ارجاع‌های Rvalue نام‌دار Lvalue هستند

مشکل اساسی که ارسال کامل (perfect forwarding) حل می‌کند اینجاست: وقتی آرگومانی را به صورت T&& می‌پذیرید و به تابع دیگری ارسال می‌کنید، نام آن را lvalue می‌کند:

void inner(int& x)  { std::cout << "lvalue\n"; }
void inner(int&& x) { std::cout << "rvalue\n"; }

void outer(int&& x) {
    inner(x);  // inner(int&) را فراخوانی می‌کند — x یک lvalue است!
}

دسته مقدار در هر مرز تابع از دست می‌رود. اگر یک تابع پوشش‌دهنده (wrapper) می‌نویسید (ثبت وقایع، زمان‌سنجی، کارخانه)، باید آرگومان را با حفظ دسته مقدار اصلی ارسال کنید. ارسال ساده x همیشه lvalue ارسال می‌کند. استفاده از std::move(x) همیشه rvalue ارسال می‌کند. هیچ‌کدام برای پوشش‌دهنده عمومی صحیح نیست.

std::forward: تبدیل نوع شرطی

std::forward(x) یک تبدیل نوع شرطی است:

- اگر T یک ارجاع lvalue باشد (چون lvalue ارسال شده بود)، std::forward(x) یک ارجاع lvalue برمی‌گرداند — بدون تبدیل.
- اگر T یک غیرارجاع باشد (چون rvalue ارسال شده بود)، std::forward(x) یک ارجاع rvalue برمی‌گرداند — به T&& تبدیل می‌کند.

تفاوت کلیدی با std::move:
- std::move همیشه به rvalue تبدیل می‌کند (بدون شرط).
- std::forward فقط اگر آرگومان اصلی rvalue بود به rvalue تبدیل می‌کند (شرطی).

std::forward فقط باید با ارجاع‌های ارسال (T&& که T استنتاج‌شده) استفاده شود. استفاده از آن در جای دیگر باگ است.

الگوی استاندارد ارسال

الگو: ارجاع ارسال T&& را بپذیرید، سپس از std::forward(arg) برای حفظ دسته مقدار هنگام ارسال به تابع دیگر استفاده کنید.

perfect_forwarding.cpp
#include <iostream>
#include <string>
#include <utility>
#include <chrono>

// The function we want to wrap
void process(const std::string& s) {
    std::cout << "process(lvalue): " << s << '\n';
}
void process(std::string&& s) {
    std::cout << "process(rvalue): " << s << '\n';
    // Can steal s's resources since it's an rvalue
}

// Generic wrapper that perfectly forwards
template <typename T>
void timed_process(T&& arg) {
    auto start = std::chrono::steady_clock::now();

    // std::forward<T>(arg) preserves the original value category
    process(std::forward<T>(arg));

    auto elapsed = std::chrono::steady_clock::now() - start;
    std::cout << "Took: " << elapsed.count() << " ns\n";
}

int main() {
    std::string name = "Alice";
    timed_process(name);              // T = string&,  forwards as lvalue
    timed_process(std::string("Bob")); // T = string, forwards as rvalue
    timed_process("Charlie");          // T = const char(&)[8], forwards as lvalue
}

ارسال متغیر و emplace_back

ارسال کامل واقعاً با قالب‌های متغیر (variadic templates) می‌درخشد. emplace_back آرگومان‌های سازنده را مستقیماً به درون ظرف ارسال می‌کند و شیء را در محل بدون هیچ کپی یا انتقالی می‌سازد.

variadic_forwarding.cpp
#include <iostream>
#include <vector>
#include <string>
#include <utility>

struct Employee {
    std::string name;
    int age;
    Employee(std::string n, int a) : name(std::move(n)), age(a) {
        std::cout << "Constructed: " << name << '\n';
    }
    Employee(const Employee&) { std::cout << "Copied\n"; }
    Employee(Employee&&) noexcept { std::cout << "Moved\n"; }
};

// Custom factory using variadic forwarding
template <typename T, typename... Args>
std::vector<T> make_vector_of(Args&&... args) {
    std::vector<T> v;
    // Fold expression (C++17) + emplace_back
    (v.emplace_back(std::forward<Args>(args)), ...);
    return v;
}

int main() {
    std::vector<Employee> team;

    // push_back: creates temporary, then MOVES it into vector
    team.push_back(Employee("Alice", 30));
    // Output: Constructed: Alice, Moved

    // emplace_back: constructs IN-PLACE — no copy, no move
    team.emplace_back("Bob", 25);
    // Output: Constructed: Bob

    std::cout << "---\n";
    // Variadic forwarding
    auto v = make_vector_of<std::string>("hello", std::string("world"), "!");
    for (const auto& s : v) std::cout << s << ' ';
    std::cout << '\n';
}

move در مقابل forward: تفاوت را بدانید

| | std::move | std::forward |
|---|---|---|
| نوع تبدیل | تبدیل بدون شرط به T&& | شرطی — lvalue اگر T برابر T& باشد، rvalue اگر T برابر T باشد |
| مورد استفاده | «کارم با این تمام شده، بربایش» | «این را با دسته مقدار اصلی‌اش ارسال کن» |
| زمینه | انواع مشخص، آخرین استفاده از متغیر | فقط ارجاع‌های ارسال قالب |
| خطر | انتقال تصادفی مقداری که هنوز نیازش دارید | استفاده بدون ارجاع ارسال (بی‌معنی) |

قاعده کلی: از std::move استفاده کنید وقتی مقدار را در اختیار دارید و می‌خواهید واگذارش کنید. از std::forward استفاده کنید وقتی مقدار کس دیگری را ارسال می‌کنید.

دام

۱. ارسال بیش از یک بار: std::forward(arg) ممکن است به rvalue تبدیل کند. اگر همان آرگومان را به دو تابع ارسال کنید، اولی ممکن است منابعش را برباید و برای دومی زباله بگذارد. هر آرگومان را حداکثر یک بار ارسال کنید.

۲. auto&& یک ارجاع ارسال است: for (auto&& elem : container) دسته مقدار را استنتاج می‌کند. این برای range-for مشکلی ندارد اما در زمینه‌های دیگر هنگام استفاده از auto&& مراقب باشید.

۳. std::forward بدون استنتاج: std::forward(s) فقط std::move(s) در لباس مبدل است. فقط از std::forward با پارامتر قالب استنتاج‌شده استفاده کنید.

۴. لیست‌های مقداردهی آکلادی: {1, 2, 3} نوعی ندارد و قابل ارسال نیست. ابتدا به متغیری تخصیص دهید.

بهترین شیوه

1. فقط با ارجاع‌های ارسال از std::forward استفاده کنید (T&& که T استنتاج‌شده).
2. هر آرگومان را حداکثر یک بار ارسال کنید — ارسال ممکن است منتقل کند و منبع را خالی بگذارد.
3. emplace_back را بر push_back ترجیح دهید هنگام ساخت اشیاء در ظروف.
4. در حلقه ارسال نکنید — تکرار اول ممکن است آرگومان را برباید.
5. از if constexpr با std::is_lvalue_reference_v استفاده کنید وقتی به رفتار متفاوت برای lvalue‌ها و rvalue‌ها نیاز دارید.

نکات کلیدی
  • ارجاع‌های rvalue نام‌دار lvalue هستند — دسته مقدار در مرزهای تابع از دست می‌رود
  • std::forward به‌صورت شرطی تبدیل می‌کند: lvalue به lvalue می‌ماند، rvalue به rvalue می‌ماند
  • std::move بدون شرط است (همیشه rvalue)؛ std::forward شرطی است (اصلی را حفظ می‌کند)
  • emplace_back از ارسال متغیر برای ساخت اشیاء در محل با صفر کپی استفاده می‌کند
  • هر آرگومان را حداکثر یک بار ارسال کنید — ارسال ممکن است منابع را برباید

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

(20 امتیاز)

1. تفاوت بین `std::move` و `std::forward` چیست؟

2. چرا `emplace_back` از `push_back` برای ساخت عناصر کارآمدتر است؟

3. اگر همان آرگومان را با `std::forward` به دو تابع مختلف ارسال کنید چه اتفاقی می‌افتد؟