सामग्री पर जाएं

परफ़ेक्ट फ़ॉरवर्डिंग और std::forward

समझें कि नाम वाले rvalue रेफ़रेंस lvalue क्यों होते हैं, std::forward के साथ कैनोनिकल फ़ॉरवर्डिंग पैटर्न सीखें, और देखें कि emplace_back वेरिएडिक फ़ॉरवर्डिंग से ऑब्जेक्ट को इन-प्लेस कैसे बनाता है।

समस्या: नाम वाले Rvalue रेफ़रेंस Lvalue होते हैं

परफ़ेक्ट फ़ॉरवर्डिंग जिस बुनियादी समस्या को हल करती है, वह यह है: जब आप कोई आर्ग्युमेंट 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) एक सशर्त कास्ट (conditional cast) है:

- अगर 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

परफ़ेक्ट फ़ॉरवर्डिंग की असली ताक़त वेरिएडिक टेम्पलेट के साथ दिखती है। 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&& में बिना शर्त कास्ट | सशर्त — T के T& होने पर lvalue, T के T होने पर rvalue |
| उपयोग का मामला | "मेरा काम हो गया, इसे ले लो" | "इसे इसकी मूल वैल्यू कैटेगरी के साथ आगे पास करो" |
| संदर्भ | कंक्रीट टाइप, वेरिएबल का आख़िरी उपयोग | केवल टेम्पलेट फ़ॉरवर्डिंग रेफ़रेंस |
| ख़तरा | गलती से ऐसी वैल्यू मूव कर देना जिसकी आपको अब भी ज़रूरत है | फ़ॉरवर्डिंग रेफ़रेंस के बिना उपयोग करना (अर्थहीन) |

मोटा नियम: जब आप वैल्यू के मालिक हों और उसे दे देना चाहते हों, तब std::move का उपयोग करें। जब आप किसी और की वैल्यू फ़ॉरवर्ड कर रहे हों, तब std::forward का उपयोग करें।

सामान्य गलती

1. एक से ज़्यादा बार फ़ॉरवर्ड करना: std::forward(arg) rvalue में कास्ट कर सकता है। अगर आप एक ही आर्ग्युमेंट को दो फ़ंक्शन में फ़ॉरवर्ड करते हैं, तो पहला उसके रिसोर्स ले सकता है और दूसरे के लिए कचरा (garbage) छोड़ देता है। हर आर्ग्युमेंट को अधिकतम एक बार फ़ॉरवर्ड करें।

2. auto&& एक फ़ॉरवर्डिंग रेफ़रेंस है: for (auto&& elem : container) वैल्यू कैटेगरी डिड्यूस करता है। रेंज-for के लिए यह ठीक है, लेकिन अन्य संदर्भों में auto&& इस्तेमाल करते समय सावधान रहें।

3. बिना डिडक्शन के std::forward: std::forward(s) असल में भेस बदला हुआ std::move(s) ही है। std::forward का उपयोग केवल डिड्यूस किए गए टेम्पलेट पैरामीटर के साथ करें।

4. ब्रेस्ड-इनिट-लिस्ट: {1, 2, 3} का कोई टाइप नहीं होता और उसे फ़ॉरवर्ड नहीं किया जा सकता। पहले उसे किसी वेरिएबल में असाइन करें।

सर्वोत्तम अभ्यास

1. std::forward का उपयोग केवल फ़ॉरवर्डिंग रेफ़रेंस के साथ करें (T&&, जहाँ T डिड्यूस हुआ हो)।
2. हर आर्ग्युमेंट को अधिकतम एक बार फ़ॉरवर्ड करें — फ़ॉरवर्डिंग मूव कर सकती है, जिससे सोर्स खाली रह जाता है।
3. कंटेनर में ऑब्जेक्ट बनाते समय push_back की जगह emplace_back को प्राथमिकता दें।
4. लूप में फ़ॉरवर्ड न करें — पहला इटरेशन आर्ग्युमेंट को ले सकता है।
5. जब lvalue और rvalue के लिए अलग व्यवहार चाहिए, तब std::is_lvalue_reference_v के साथ if constexpr का उपयोग करें।

मुख्य बातें
  • नाम वाले rvalue रेफ़रेंस lvalue होते हैं — फ़ंक्शन की सीमाओं पर वैल्यू कैटेगरी खो जाती है
  • std::forward सशर्त कास्ट करता है: lvalue, lvalue ही रहता है और rvalue, rvalue ही रहता है
  • std::move बिना शर्त है (हमेशा rvalue); std::forward सशर्त है (मूल को बनाए रखता है)
  • emplace_back वेरिएडिक फ़ॉरवर्डिंग से ऑब्जेक्ट को शून्य कॉपी के साथ इन-प्लेस बनाता है
  • हर आर्ग्युमेंट को अधिकतम एक बार फ़ॉरवर्ड करें — फ़ॉरवर्डिंग रिसोर्स ले सकती है

क्विज़ — अपना ज्ञान परखें

(20 XP)

1. `std::move` और `std::forward` में क्या अंतर है?

2. एलिमेंट बनाने के लिए `emplace_back`, `push_back` से ज़्यादा कुशल क्यों है?

3. अगर आप एक ही आर्ग्युमेंट को दो अलग-अलग फ़ंक्शन में `std::forward` करें तो क्या होता है?