Идеальная передача (Perfect Forwarding)
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy — урок 84 из 104.
При написании шаблонных функций, которые принимают аргументы и передают их другим функциям, возникает проблема: как сохранить информацию о том, был ли исходный аргумент lvalue или rvalue? Идеальная передача (Perfect forwarding) решает эту проблему, передавая аргументы именно в том виде, в котором они были получены, сохраняя их категорию значения.
Ключевыми компонентами являются пересылающие ссылки (записываемые как T&& в контексте шаблона) и std::forward. Пересылающая ссылка может привязываться как к lvalue, так и к rvalue, а std::forward условно приводит аргумент обратно к его исходному типу:
#include <iostream>
#include <utility>
void process(int& x) { std::cout << "lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue: " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int main() {
int n = 10;
wrapper(n); // Вызывает process(int&) - lvalue сохранено
wrapper(20); // Вызывает process(int&&) - rvalue сохранено
}Без std::forward именованные параметры внутри функции всегда являются lvalue, даже если было передано rvalue. Это предотвратило бы правильную работу семантики перемещения при передаче в конструкторы или другие функции.
Идеальная передача (perfect forwarding) необходима для написания фабричных функций, классов-оберток и любого обобщенного кода, которому требуется конструировать объекты или вызывать функции, сохраняя при этом намерение вызывающей стороны в отношении перемещения или копирования.
Задание
ЛегкоДавайте создадим функцию-фабрику, которая демонстрирует идеальную пересылку (perfect forwarding), создавая объекты и сохраняя категорию значения (value category) переданных ей аргументов. Вы создадите универсальную функцию make_object, которая пересылает аргументы конструктору класса, гарантируя, что lvalue копируются, а rvalue перемещаются.
Вы организуете свой код в трех файлах:
Widget.h: Определите классWidget, который отслеживает, как он получает свои данные.Ваш
Widgetдолжен хранить имя типаstd::stringи значение типаint. Предусмотрите два конструктора:- Один, который принимает
const std::string&(lvalue-ссылка) иint— выводит"Widget constructed (copy): [name]" - Один, который принимает
std::string&&(rvalue-ссылка) иint— выводит"Widget constructed (move): [name]"и перемещает строку
Также добавьте метод
display(), который выводит"[name]: [value]".- Один, который принимает
Factory.h: Создайте свою функцию-фабрику с идеальной пересылкой.Напишите шаблонную функцию
make_object, которая принимает универсальную ссылку (forwarding reference) для имени и обычныйintдля значения. Используйтеstd::forwardдля передачи аргумента имени в конструкторWidget, сохраняя его исходную категорию значения.Функция должна возвращать созданный
Widgetпо значению.main.cpp: Считайте два входных значения:- Имя (строка)
- Значение (целое число)
Продемонстрируйте идеальную пересылку, создав два виджета:
- Сначала сохраните имя в локальной переменной и передайте его в
make_objectкак lvalue - Затем передайте временную строку (то же имя с добавлением
"_temp") напрямую вmake_objectкак rvalue
После создания каждого виджета вызовите его метод
display().
Например, при входных данных Gadget и 42:
Widget constructed (copy): Gadget
Gadget: 42
Widget constructed (move): Gadget_temp
Gadget_temp: 42При входных данных Device и 100:
Widget constructed (copy): Device
Device: 100
Widget constructed (move): Device_temp
Device_temp: 100Ключевая идея заключается в том, что ваша функция make_object не знает, получила ли она lvalue или rvalue, но, используя std::forward, она сохраняет эту информацию при вызове конструктора Widget. Это позволяет механизму разрешения перегрузки конструктора выбрать соответствующую версию, обеспечивая эффективное перемещение для временных объектов и безопасное копирование именованных переменных.
Шпаргалка
Идеальная передача (perfect forwarding) позволяет шаблонным функциям передавать аргументы другим функциям, сохраняя их исходную категорию значения (lvalue или rvalue).
Этот механизм использует пересылающие ссылки (T&& в контексте шаблона) в сочетании с std::forward:
#include <utility>
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}Пересылающая ссылка может привязываться как к lvalue, так и к rvalue. std::forward<T> условно приводит аргумент обратно к его исходному типу.
Без std::forward именованные параметры внутри функции всегда рассматриваются как lvalue, даже если было передано rvalue. Это помешало бы правильной работе семантики перемещения.
Пример, демонстрирующий сохранение категории значения:
void process(int& x) { std::cout << "lvalue: " << x << "\n"; }
void process(int&& x) { std::cout << "rvalue: " << x << "\n"; }
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
int n = 10;
wrapper(n); // Calls process(int&) - lvalue preserved
wrapper(20); // Calls process(int&&) - rvalue preservedИдеальная передача необходима для фабричных функций, классов-оберток и обобщенного кода, который создает объекты или вызывает функции, сохраняя намерение вызывающей стороны относительно перемещения или копирования.
Попробуйте сами
#include <iostream>
#include <string>
#include "Widget.h"
#include "Factory.h"
using namespace std;
int main() {
// Чтение входных данных
string name;
int value;
cin >> name >> value;
// TODO: Создайте первый виджет, передав name как lvalue
// Сохраните имя в локальную переменную и передайте её в make_object
// TODO: Вызовите display() для первого виджета
// TODO: Создайте второй виджет, передав временную строку как rvalue
// Передайте (name + "_temp") напрямую в make_object
// TODO: Вызовите display() для второго виджета
return 0;
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП
Внешние файлыСборка и компиляция C++Заголовочные файлы и файлы исходного кодаПространства имен и область видимостиВведение в ООП на C++Классы и объектыУказатель 'this'Методы (функции-члены)Атрибуты (члены данных)Основы конструкторов и деструкторовИтоги — Простой калькулятор4Свойства классов
Члены экземпляра против статическихГеттеры и сеттерыКонстантные функции-членыКлючевое слово mutableСтатические методы и переменныеДружественные функции и классыИтоги — Менеджер банковских счетов7Наследование
Основы наследованияУровни доступа при наследованииПорядок вызова конструкторов и деструкторовПереопределение методовВиртуальные функции и VTableМножественное наследованиеВиртуальное наследованиеИтоги — Иерархия сотрудников10Обзор STL
Обзор и философия STLКонтейнеры STLИтераторыАлгоритмы STLФункторы и лямбда-выраженияИтоги: частота слов13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonFactory и Abstract FactoryПаттерн BuilderПаттерн ObserverПаттерн Strategy2Управление памятью
Стек и кучаУказатели и ссылкиДинамическая память (new/delete)Умные указатели в C++RAII в C++Итоги — Менеджер динамического массива5Инкапсуляция
Спецификаторы доступа в C++Спецификаторы доступа: подробный разборСокрытие данныхStruct против ClassВложенные и внутренние классыИтоги — Система учета студентов8Полиморфизм
Полиморфизм компиляции и времени выполненияПерегрузка функцийВиртуальные функции: повторениеЧистые виртуальные функцииАбстрактные классыПроектирование интерфейсов на C++Dynamic Casting и RTTIИтоги: Калькулятор фигур11Продвинутые концепции ООП
Композиция против наследованияМиксины через CRTPИдиома PimplСтирание типовEnum Classes и строгая типизацияОбработка исключений в ООППользовательские иерархии исключений14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикRAII как паттерн3Конструкторы и деструкторы
Конструктор по умолчаниюПараметризованный конструкторКонструктор копированияКонструктор перемещенияСписки инициализации конструктораДелегирующие конструкторыГлубокое погружение в деструкторыПравило трех / пяти / нуляПовторение — класс String6Перегрузка операторов
Введение в перегрузку операторовПерегрузка арифметических операторовПерегрузка операторов сравненияОператоры потоковПерегрузка оператора присваиванияПерегрузка операторов [] и ()Операторы преобразования типовИтоги — класс Matrix9Шаблоны
Шаблоны функцийШаблоны классовСпециализация шаблоновВариативные шаблоныОсновы SFINAE и Type TraitsИтоги — Обобщенный контейнер12Возможности современного C++
Семантика перемещения и RvaluesИдеальная передача (Perfect Forwarding)Лямбда-выражения в деталяхstd::function и std::bindconstexpr и constevalСтруктурированные привязкиoptional, variant, any