ارجاعها: Lvalue، Rvalue و دامها
ارجاعهای lvalue و rvalue را بررسی کنید، دستهبندی مقادیر C++ را درک کنید، قوانین فروپاشی ارجاع و ارجاعهای ارسال (forwarding references) را بیاموزید و از باگهای ارجاع معلق اجتناب کنید.
ارجاعهای Lvalue
یک ارجاع lvalue (T&) یک نام مستعار است — نام دیگری برای یک شیء موجود. برخلاف اشارهگرها، ارجاعها باید مقداردهی اولیه شوند، نمیتوانند null باشند و نمیتوانند به شیء دیگری تغییر مقصد دهند.
ارجاعهای lvalue به lvalueها متصل میشوند — عبارتهایی که هویت دارند (میتوانید آدرسشان را بگیرید). متغیرها، عناصر آرایه و اشارهگرهای ارجاعزداییشده همگی lvalue هستند. یک ارجاع lvalue غیر const (int&) نمیتواند به یک موقتی (rvalue) متصل شود زیرا تغییر یک موقتی تقریباً همیشه یک باگ است.
اصول ارجاع Lvalue
ارجاعهای lvalue ابزار اصلی C++ هستند — برای ارسال با ارجاع (pass-by-reference)، سربارگذاری عملگرها و اجتناب از کپی استفاده میشوند.
#include <iostream>
#include <string>
void append_greeting(std::string& s) {
s += ", welcome!"; // modifies the caller's string
}
int main() {
int x = 10;
int& ref = x; // ref is an alias for x
ref = 20;
std::cout << x << '\n'; // 20 — x was modified through ref
// const reference can bind to a temporary (rvalue)
const std::string& greeting = std::string("Hello");
// The temporary's lifetime is extended to match greeting's scope
std::cout << greeting << '\n'; // "Hello"
// Non-const lvalue reference CANNOT bind to rvalue:
// std::string& bad = std::string("oops"); // COMPILER ERROR
std::string name = "Alice";
append_greeting(name);
std::cout << name << '\n'; // "Alice, welcome!"
}دستهبندی مقادیر در C++
C++ طبقهبندی دقیقی از دستهبندی مقادیر عبارات دارد. درک آنها برای معناشناسی انتقال (move semantics) و ارسال کامل (perfect forwarding) ضروری است:
- lvalue — هویت دارد، نمیتوان بهصورت ضمنی از آن منتقل کرد. مثالها: متغیرها، *ptr، arr[i]، رشتههای لیترال.
- prvalue (rvalue خالص) — هویت ندارد، میتوان از آن منتقل کرد. مثالها: 42، x + y، std::string("temp")، تابعی که با مقدار برمیگرداند.
- xvalue (مقدار در حال انقضا) — هویت دارد و میتوان از آن منتقل کرد. مثالها: std::move(x)، static_cast، عضو یک rvalue.
- glvalue (lvalue تعمیمیافته) — lvalue یا xvalue (هویت دارد).
- rvalue — prvalue یا xvalue (قابل انتقال است).
نکته کلیدی: یک متغیر ارجاع rvalue خود یک lvalue است — نام و آدرس دارد. این منبع بسیاری از سردرگمیهاست و دقیقاً دلیل وجود std::forward است.
ارجاعهای Rvalue (T&&)
ارجاعهای rvalue به rvalueها (موقتیها و اشیاء std::move شده) متصل میشوند. آنها پایه و اساس معناشناسی انتقال هستند — به شما اجازه میدهند تشخیص دهید که یک شیء در آستانه نابودی است و منابع آن را بربایید.
#include <iostream>
#include <string>
#include <utility> // std::move
void process(const std::string& s) {
std::cout << "lvalue: " << s << '\n';
}
void process(std::string&& s) {
std::cout << "rvalue: " << s << '\n';
// s is an lvalue inside this function (it has a name!)
// To move from s, you'd need std::move(s)
}
int main() {
std::string a = "hello";
process(a); // calls lvalue overload
process(std::string("world")); // calls rvalue overload
process(std::move(a)); // calls rvalue overload — a is now moved-from
// IMPORTANT: a is in a valid but unspecified state
// Only safe operations: assign to it, destroy it
std::cout << "a after move: '" << a << "'\n"; // likely empty
}فروپاشی ارجاع و ارجاعهای ارسال
هنگامی که ارجاعها در قالبها (template) ترکیب میشوند، قوانین فروپاشی ارجاع (reference collapsing) اعمال میشود:
- T& & به T& فرومیپاشد
- T& && به T& فرومیپاشد
- T&& & به T& فرومیپاشد
- T&& && به T&& فرومیپاشد
قاعده ساده است: اگر هر یک از ارجاعها ارجاع lvalue باشد، نتیجه ارجاع lvalue است. فقط && && نتیجه && میدهد.
یک ارجاع ارسال (forwarding reference، که ارجاع عمومی هم نامیده میشود) T&& است که T یک پارامتر قالب استنتاجشده باشد. میتواند هم به lvalueها و هم به rvalueها متصل شود. auto&& نیز یک ارجاع ارسال است. اما std::vector اینطور نیست — T از قبل مشخص است. const T&& نیز ارجاع ارسال نیست.
یک ارجاع معلق (dangling reference) به شیء نابودشدهای ارجاع میدهد. برخلاف اشارهگرهای معلق، بررسی nullptr وجود ندارد — کد بیصدا از دادههای زباله استفاده میکند یا از کار میافتد.
علل رایج:
- بازگرداندن ارجاع به متغیر محلی — متغیر هنگام بازگشت تابع نابود میشود.
- گرفتن با ارجاع در لامبدا که عمر بیشتری از متغیر ارجاعشده دارد.
- const auto& به موقتی درون عبارت — const auto& val = get_map()["key"] معلق است اگر get_map() با مقدار برگرداند، زیرا نقشه (map) موقتی در نقطهویرگول نابود میشود.
- حلقه range-based for روی موقتی — for (auto& x : get_vector()) مشکلی ندارد (موقتی تا پایان حلقه زنده میماند)، اما for (auto& x : get_wrapper().get_vector()) ممکن است معلق باشد اگر wrapper نابود شود.
تمدید طول عمر با const T&
یک ارجاع lvalue از نوع const میتواند طول عمر یک موقتی را تمدید کند، اما فقط زمانی که مستقیماً متصل باشد. این از طریق فراخوانی تابع یا زنجیرههای دسترسی عضو کار نمیکند.
#include <iostream>
#include <string>
std::string make_greeting() { return "Hello, World!"; }
const std::string& identity(const std::string& s) { return s; }
int main() {
// SAFE: const ref directly bound to temporary
const std::string& good = make_greeting();
std::cout << good << '\n'; // OK — lifetime extended
// DANGEROUS: lifetime extension does NOT propagate through functions
const std::string& bad = identity(make_greeting());
// bad is DANGLING — the temporary was destroyed after identity() returned
// std::cout << bad << '\n'; // UNDEFINED BEHAVIOR
// SAFE: just use a value
std::string safe = make_greeting(); // move or copy-elision
std::cout << safe << '\n'; // always safe
}- ارجاعهای lvalue (
T&) نام مستعار هستند — باید مقداردهی شوند، نمیتوانند null باشند یا تغییر مقصد دهند const T&میتواند به موقتیها متصل شود و طول عمرشان را تمدید کند (اما فقط وقتی مستقیماً متصل باشد)- ارجاعهای rvalue (
T&&) به موقتیها متصل میشوند و معناشناسی انتقال را ممکن میسازند - یک ارجاع rvalue نامدار خود یک lvalue است — درک این نکته حیاتی است
- فروپاشی ارجاع: هر
&در زنجیره نتیجه را&میکند - ارجاعهای ارسال (
T&&باTاستنتاجشده) هم به lvalueها و هم به rvalueها متصل میشوند
آزمون — دانش خود را بسنجید
(15 امتیاز)1. با فرض `void f(std::string&& s)`، دسته مقدار `s` درون `f` چیست؟
2. کدام قاعده فروپاشی ارجاع صحیح است؟
3. تمدید طول عمر `const T&` برای موقتیها چه زمانی کار میکند؟