Přeskočit na obsah

Perfect Forwarding & std::forward

Pochopte, proč jsou pojmenované odkazy rvalue lvalue, naučte se kanonický vzor předávání pomocí std::forward a podívejte se, jak emplace_back používá varadické předávání k vytváření objektů na místě.

Problém: Pojmenované reference Rvalue jsou Lvalues

Zde je základní problém, který dokonalé předávání řeší: když přijmete argument jakoT&&a předáte jej jiné funkci, název z něj udělá lvalue:

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) vždy předává hodnotu r. Ani jedno není správné pro obecný obal.

std::forward: The Conditional Cast

std::forward(x)je podmíněné obsazení:

- Pokud jeTlvalue reference (odvozeno, protože byla předána lvalue),std::forward(x)vrátí lvalue reference – bez přetypování.
- Pokud jeTnereference (odvozeno, protože byla předána rvalue),std::forward(x)vrátí referenci rvalue – přetypuje naT&&.

Klíčový rozdíl odstd::move:
-std::movevždy vrhá na rvalue (bezpodmínečné).
-std::forwardpřetypuje na rvalue pouze v případě, že původní argument byl rvalue (podmíněný).

std::forwardby se měl používat POUZE s odkazy na přesměrování (T&&, kde je odvozenoT). Použití jinde je chyba.

Canonical Forwarding Pattern

Vzor: přijměte odkaz pro předáváníT&&a poté použijtestd::forward(arg)k zachování kategorie hodnot při předání jiné funkci.

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

Perfektní přeposílání skutečně září s variadic šablonami.emplace_backpředává argumenty konstruktoru přímo do kontejneru a vytváří objekt na místě bez jakýchkoli kopií nebo přesunů.

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';
}

posun vs vpřed: Poznej rozdíl

| |std::move|std::forward|
|---|---|---|
| Typ obsazení | Bezpodmínečné obsazení doT&&| Podmíněné — lvalue, pokudTjeT&, rvalue, pokudTjeT|
| Případ použití | "Skončil jsem s tím, ukradni to" | "Předejte to spolu s jeho původní hodnotovou kategorií" |
| Kontext | Konkrétní typy, poslední použití proměnné | Pouze odkazy na předávání šablon |
| Nebezpečí | Náhodný přesun hodnoty, kterou stále potřebujete | Použití bez odkazu na přesměrování (bezvýznamné) |

Pravidlo: použijtestd::move, když vlastníte hodnotu a chcete ji darovat. Použijtestd::forward, když přeposíláte hodnotu někoho jiného.

Úskalí

1. Přesměrování více než jednou:std::forward(arg)může přenášet na rvalue. Pokud předáte stejný argument dvěma funkcím, první může ukrást její zdroje a zanechat odpadky pro druhou. Přeposlat každý argument maximálně jednou.

2.auto&&je odkaz pro předávání:for (auto&& elem : container)vyvozuje kategorii hodnoty. To je v pořádku pro range-for, ale buďte opatrní, když používáteauto&&v jiných kontextech.

3.std::forwardbez srážky:std::forward(s)je pouze maskovanýstd::move(s).std::forwardpoužívejte pouze s odvozeným parametrem šablony.

4. Braced-init-lists:{1, 2, 3}nemá žádný typ a nelze jej přeposlat. Nejprve přiřadit k proměnné.

Osvědčený postup

1. Používejtestd::forwardpouze s referencemi pro přesměrování (T&&, kdeTje odvozeno).
2. Přepošlete každý argument maximálně jednou — přeposlání se může přesunout a zdroj zůstane prázdný.
3. Při konstrukci objektů v kontejnerech dejte přednostemplace_backpředpush_back.
4. Nepřeposílání ve smyčce — první iterace může ukrást argument.
5. Použijteif constexprsstd::is_lvalue_reference_v, když potřebujete odlišné chování pro lvalues ​​a rvalues.

Klíčové poznatky
  • Pojmenované odkazy rvalue jsou lvalues ​​— kategorie hodnot se ztrácí na hranicích funkcí
  • std::forwardpodmíněně vrhá: lvalue zůstává lvalue, rvalue zůstává rvalue
  • std::moveje bezpodmínečné (vždy rvalue);std::forwardje podmíněný (zachovává originál)
  • emplace_backpoužívá variadic forwarding k vytvoření objektů na místě s nulovými kopiemi
  • Přepošlete každý argument maximálně jednou – přeposlání může ukrást zdroje

Kvíz — Otestujte své znalosti

(20 XP)

1. Jaký je rozdíl mezi`std::move`a`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?