ارسال کامل و 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 یک تبدیل نوع شرطی است:
- اگر T یک ارجاع lvalue باشد (چون lvalue ارسال شده بود)، std::forward یک ارجاع lvalue برمیگرداند — بدون تبدیل.
- اگر T یک غیرارجاع باشد (چون rvalue ارسال شده بود)، std::forward یک ارجاع rvalue برمیگرداند — به T&& تبدیل میکند.
تفاوت کلیدی با std::move:
- std::move همیشه به rvalue تبدیل میکند (بدون شرط).
- std::forward فقط اگر آرگومان اصلی rvalue بود به rvalue تبدیل میکند (شرطی).
std::forward فقط باید با ارجاعهای ارسال (T&& که T استنتاجشده) استفاده شود. استفاده از آن در جای دیگر باگ است.
الگوی استاندارد ارسال
الگو: ارجاع ارسال T&& را بپذیرید، سپس از std::forward برای حفظ دسته مقدار هنگام ارسال به تابع دیگر استفاده کنید.
#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 آرگومانهای سازنده را مستقیماً به درون ظرف ارسال میکند و شیء را در محل بدون هیچ کپی یا انتقالی میسازد.
#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 ممکن است به rvalue تبدیل کند. اگر همان آرگومان را به دو تابع ارسال کنید، اولی ممکن است منابعش را برباید و برای دومی زباله بگذارد. هر آرگومان را حداکثر یک بار ارسال کنید.
۲. auto&& یک ارجاع ارسال است: for (auto&& elem : container) دسته مقدار را استنتاج میکند. این برای range-for مشکلی ندارد اما در زمینههای دیگر هنگام استفاده از auto&& مراقب باشید.
۳. std::forward بدون استنتاج: std::forward فقط 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` به دو تابع مختلف ارسال کنید چه اتفاقی میافتد؟