Μετάβαση στο περιεχόμενο

Perfect Forwarding & std::forward

Κατανοήστε γιατί οι ονομασμένες αναφορές rvalue είναι lvalues, μάθετε το κανονικό μοτίβο προώθησης με το std::forward και δείτε πώς το emplace_back χρησιμοποιεί μεταβλητή προώθηση για τη δημιουργία αντικειμένων επιτόπου.

Το πρόβλημα: Οι αναφορές με όνομα Rvalue είναι Lvalues

Εδώ είναι το βασικό πρόβλημα που λύνει η τέλεια προώθηση: όταν αποδέχεστε ένα όρισμα ωςT&&και το μεταβιβάζετε σε άλλη συνάρτηση, το όνομα το κάνει lvalue:

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

void outer(int&& x) {
inner(x); // calls inner(int&) — x is an lvalue!
}
```

The value category is lost at every function boundary. If you are writing a wrapper function (logging, timing, factory), you need to forward the argument with its original value category preserved. Simply passing x always passes an lvalue. Using std::move(x) περνάει πάντα μια τιμή rvalue. Κανένα από τα δύο δεν είναι σωστό για ένα γενικό περιτύλιγμα.

std::forward: The Conditional Cast

Τοstd::forward(x)είναι cast υπό όρους:

- Εάν τοTείναι μια lvalue αναφορά (συνάγεται επειδή μεταβιβάστηκε μια lvalue), τοstd::forward(x)επιστρέφει μια lvalue αναφορά — χωρίς cast.
- Εάν το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
}

Variadic Forwarding & 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';
}

κίνηση εναντίον εμπρός: Μάθετε τη διαφορά

| |std::move|std::forward|
|---|---|---|
| Τύπος καστ | Μετάδοση άνευ όρων στοT&&| Υπό όρους — lvalue εάν τοTείναιT&, rvalue εάν τοTείναιT|
| Περιπτώσεις χρήσης | «Τελείωσα με αυτό, κλέψτε το» | "Περάστε αυτό μαζί με την αρχική του κατηγορία αξίας" |
| Πλαίσιο | Τύποι σκυροδέματος, τελευταία χρήση μεταβλητής | Μόνο αναφορές προώθησης προτύπων |
| Κίνδυνος | Κατά λάθος μετακίνηση μιας τιμής που χρειάζεστε ακόμα | Χρήση χωρίς αναφορά προώθησης (χωρίς νόημα) |

Εμπειρικός κανόνας: χρησιμοποιήστε τοstd::moveόταν κατέχετε την αξία και θέλετε να τη χαρίσετε. Χρησιμοποιήστε τοstd::forwardόταν προωθείτε την αξία κάποιου άλλου.

Παγίδα

1. Προώθηση περισσότερες από μία φορές: Τοstd::forward(arg)μπορεί να μεταδοθεί σε rvalue. Εάν προωθήσετε το ίδιο όρισμα σε δύο συναρτήσεις, η πρώτη μπορεί να κλέψει τους πόρους της, αφήνοντας σκουπίδια για τη δεύτερη. Προωθήστε κάθε όρισμα το πολύ μία φορά.

2. Τοauto&&είναι μια αναφορά προώθησης: Τοfor (auto&& elem : container)συνάγει την κατηγορία τιμής. Αυτό είναι καλό για εύρος, αλλά να είστε προσεκτικοί όταν χρησιμοποιείτε τοauto&&σε άλλα περιβάλλοντα.

3.std::forwardχωρίς έκπτωση: Τοstd::forward(s)είναι απλώςstd::move(s)στη μεταμφίεση. Χρησιμοποιήστε μόνο τοstd::forwardμε μια συναγόμενη παράμετρο προτύπου.

4. Braced-init-lists: Το{1, 2, 3}δεν έχει τύπο και δεν μπορεί να προωθηθεί. Αντιστοιχίστε πρώτα σε μια μεταβλητή.

Βέλτιστη πρακτική

1. Χρησιμοποιήστεstd::forwardμόνο με αναφορές προώθησης (T&&όπου συμπεραίνεται τοT).
2. Προωθήστε κάθε όρισμα το πολύ μία φορά — η προώθηση μπορεί να μετακινηθεί, αφήνοντας την πηγή κενή.
3. Προτιμήστε τοemplace_backέναντι τουpush_back όταν κατασκευάζετε αντικείμενα σε κοντέινερ.
4. Μην προωθείτε σε βρόχο — η πρώτη επανάληψη μπορεί να κλέψει το όρισμα.
5. Χρησιμοποιήστε τοif constexprμε τοstd::is_lvalue_reference_v όταν χρειάζεστε διαφορετική συμπεριφορά για lvalues ​​και rvalues.

Βασικά συμπεράσματα
  • Οι ονομασμένες αναφορές rvalue είναι lvalues ​​— η κατηγορία τιμών χάνεται στα όρια συνάρτησης
  • Τοstd::forwardεκπέμπει υπό όρους: το lvalue παραμένει lvalue, το rvalue παραμένει το rvalue
  • std::moveis unconditional (always rvalue);std::forwardis conditional (preserves original)
  • emplace_backuses variadic forwarding to construct objects in-place with zero copies
  • Forward each argument at most once — forwarding may steal resources

Κουίζ — Δοκιμάστε τις γνώσεις σας

(20 XP)

1. Ποια είναι η διαφορά μεταξύ`std::move`και`std::forward`;

2. Why is`emplace_back`more efficient than`push_back`for constructing elements?

3. What happens if you`std::forward`the same argument to two different functions?