Перейти к содержимому

Идеальное продвижение и std::forward

Поймите, почему ссылки на rvalue являются lvalues, изучите канонический паттерн пересылки с помощью std::forward и посмотрите, как emplace back использует вариадную пересылку для создания объектов на месте.

Проблема: Названные ссылки на значение являются значениями

Вот основная проблема, которую решает идеальный переадресация: когда вы принимаете аргумент в качестве T&& и передаете его другой функции, имя ** делает его значением:

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!
}

Категория значений Lost на каждой границе функций. Если вы пишете функцию wrapper (заготовка, хронометраж, фабрика), вам нужно передать аргумент с сохранением его первоначальной категории стоимости. Простое прохождение x всегда проходит значение l. При использовании std::move(x) всегда проходит значение r. Также не подходит для универсальной обертки.

Оригинальное название: Forward: The Conditional Cast

std::forward(x) является условным актерским составом:

Если T является lvalue reference (выведен из-за прохождения lvalue), std::forward(x) возвращает lvalue reference — без литья.
- Если T является нессылкой (выведена из-за прохождения r-значения), std::forward(x) возвращает ссылку на значение — отливки на T&&.

Ключевые отличия от std::move:
std::move всегда отбрасывает на rзначение (безусловно).
- 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
}

Вариадическое перемещение и место 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';
}

Продвижение vs вперед: Знайте разницу

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, когда вам нужно различное поведение для значений l и r.

Ключевые выводы
  • Названные rvalue-ссылки являются lvalues — категория значений теряется на границах функций.
  • 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` один и тот же аргумент для двух разных функций?