रेफ़रेंस: Lvalue, Rvalue और गलतियाँ
lvalue और rvalue रेफ़रेंस को जानें, C++ की वैल्यू कैटेगरी (value category) समझें, रेफ़रेंस कोलैप्सिंग (reference collapsing) के नियम और फ़ॉरवर्डिंग रेफ़रेंस (forwarding reference) सीखें, और डैंगलिंग रेफ़रेंस वाले बग से बचें।
Lvalue रेफ़रेंस
Lvalue रेफ़रेंस (T&) एक एलियस (alias) है — किसी मौजूदा ऑब्जेक्ट का दूसरा नाम। पॉइंटर के विपरीत, रेफ़रेंस को इनिशियलाइज़ करना ज़रूरी है, वे null नहीं हो सकते, और उन्हें किसी दूसरे ऑब्जेक्ट को रेफ़र करने के लिए री-सीट नहीं किया जा सकता।
Lvalue रेफ़रेंस lvalue से बंधते हैं — ऐसे एक्सप्रेशन जिनकी पहचान (identity) होती है (यानी आप उनका एड्रेस ले सकते हैं)। वेरिएबल, ऐरे एलिमेंट और डिरेफ़रेंस किए गए पॉइंटर, सभी lvalue हैं। नॉन-const lvalue रेफ़रेंस (int&) किसी अस्थायी (rvalue) से नहीं बंध सकता, क्योंकि अस्थायी को बदलना लगभग हमेशा बग होता है।
Lvalue रेफ़रेंस की बुनियादी बातें
Lvalue रेफ़रेंस C++ के वर्कहॉर्स हैं — इनका उपयोग पास-बाय-रेफ़रेंस, ऑपरेटर ओवरलोडिंग और कॉपी से बचने के लिए होता है।
#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++ में वैल्यू कैटेगरी (Value Categories)
C++ में एक्सप्रेशन की वैल्यू कैटेगरी का एक सटीक वर्गीकरण है। मूव सिमेंटिक्स और परफ़ेक्ट फ़ॉरवर्डिंग के लिए इसे समझना ज़रूरी है:
- lvalue — इसकी पहचान (identity) होती है, इससे इम्प्लिसिट रूप से मूव नहीं किया जा सकता। उदाहरण: वेरिएबल, *ptr, arr[i], स्ट्रिंग लिटरल।
- prvalue (pure rvalue) — इसकी कोई पहचान नहीं होती, इससे मूव किया जा सकता है। उदाहरण: 42, x + y, std::string("temp"), वैल्यू द्वारा रिटर्न करने वाला फ़ंक्शन।
- xvalue (expiring value) — इसकी पहचान भी होती है और इससे मूव भी किया जा सकता है। उदाहरण: std::move(x), static_cast, किसी rvalue का सदस्य (member)।
- glvalue (generalized 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
}रेफ़रेंस कोलैप्सिंग और फ़ॉरवर्डिंग रेफ़रेंस
जब टेम्पलेट में रेफ़रेंस आपस में मिलते हैं, तो रेफ़रेंस कोलैप्सिंग (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 जाँच जैसी कोई चीज़ नहीं है — कोड चुपचाप कचरा (garbage) डेटा इस्तेमाल करेगा या क्रैश हो जाएगा।
आम कारण:
- लोकल वेरिएबल का रेफ़रेंस रिटर्न करना — फ़ंक्शन के रिटर्न होते ही वह वेरिएबल नष्ट हो जाता है।
- लैम्ब्डा में रेफ़रेंस द्वारा कैप्चर करना, जबकि लैम्ब्डा उस रेफ़र किए गए वेरिएबल से ज़्यादा समय तक जीवित रहे।
- एक्सप्रेशन के भीतर किसी अस्थायी ऑब्जेक्ट पर const auto& — अगर get_map() वैल्यू द्वारा रिटर्न करता है, तो const auto& val = get_map()["key"] डैंगलिंग है, क्योंकि अस्थायी map सेमीकोलन पर नष्ट हो जाता है।
- अस्थायी ऑब्जेक्ट पर रेंज-बेस्ड for लूप — for (auto& x : get_vector()) ठीक है (अस्थायी ऑब्जेक्ट लूप के दौरान जीवित रहता है), लेकिन अगर wrapper नष्ट हो जाए तो for (auto& x : get_wrapper().get_vector()) डैंगल कर सकता है।
const T& के साथ लाइफ़टाइम एक्सटेंशन
const lvalue रेफ़रेंस किसी अस्थायी ऑब्जेक्ट की लाइफ़टाइम बढ़ा सकता है, लेकिन केवल तब जब वह सीधे बंधा (directly bound) हो। यह फ़ंक्शन कॉल या मेंबर एक्सेस की श्रृंखला के ज़रिए काम नहीं करता।
#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 XP)1. `void f(std::string&& s)` दिया गया है। `f` के भीतर `s` की वैल्यू कैटेगरी क्या है?
2. रेफ़रेंस कोलैप्सिंग का कौन-सा नियम सही है?
3. अस्थायी ऑब्जेक्ट के लिए `const T&` लाइफ़टाइम एक्सटेंशन कब काम करता है?