Идеальное продвижение и 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 является условным актерским составом:
Если T является lvalue reference (выведен из-за прохождения lvalue), std::forward возвращает lvalue reference — без литья.
- Если T является нессылкой (выведена из-за прохождения r-значения), std::forward возвращает ссылку на значение — отливки на T&&.
Ключевые отличия от std::move:std::move всегда отбрасывает на rзначение (безусловно).
- std::forward отбрасывает на rvalue только в том случае, если исходный аргумент был rvalue (условным).
std::forward следует использовать только с ссылками на пересылку (T&&, где выводится T). Использовать его в другом месте - ошибка.
Каноническая модель продвижения
Шаблон: принять ссылку пересылки T&&, затем использовать std::forward, чтобы сохранить категорию значений при переходе к другой функции.
#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 направляет аргументы конструктора непосредственно в контейнер, создавая объект на месте без каких-либо копий или движений.
#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 может отливаться до rvalue. Если направить один и тот же аргумент на две функции, то первая может украсть его ресурсы, оставив мусор на вторую. Для каждого аргумента в большинстве случаев .
2. auto&& является пересылочной ссылкой: for (auto&& elem : container) выводит категорию значений. Это хорошо для диапазона, но будьте осторожны при использовании auto&& в других контекстах.
3. std::forward без вычета: std::forward - это просто 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 остается rvaluestd::moveявляется безусловным (всегда rvalue);std::forwardявляется условным (сохраняет оригинал).emplace_backиспользует вариадную переадресацию для создания объектов на месте с нулевыми копиями- Передача каждого аргумента не более одного раза - пересылка может украсть ресурсы
Тест — проверьте свои знания
(20 XP)1. В чем разница между `std::move` и `std::forward`?
2. Почему `emplace_back` более эффективен, чем `push_back`?
3. Что произойдет, если у `std::forward` один и тот же аргумент для двух разных функций?