Идеальная передача (Perfect Forwarding)
Часть раздела Объектно-ориентированное программирование путешествия по C++ на Coddy. Урок 84 из 104.
При написании шаблонных функций, которые принимают аргументы и передают их другим функциям, возникает проблема: как сохранить информацию о том, был ли исходный аргумент lvalue или rvalue? Perfect forwarding решает эту проблему, передавая аргументы точно в том виде, в котором они были получены, сохраняя их категорию значения.
Ключевыми составляющими являются forwarding references (записываемые как T&& в контексте шаблона) и std::forward. forwarding reference может связываться как с lvalue, так и с rvalue, а std::forward условно приводит argument обратно к его исходному типу:
#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 именованные параметры внутри function всегда являются lvalue, даже если был передан rvalue. Это помешало бы корректной работе семантики перемещения при передаче в конструкторы или другие функции.
Идеальная передача необходима для написания фабричных функций, классов-обёрток и любого обобщённого кода, которому нужно конструировать объекты или вызывать функции, сохраняя намерение вызывающего кода относительно перемещения или копирования.
Задание
ЛегкоДавайте создадим Factory function, демонстрирующую идеальную передачу, создавая объекты с сохранением категории значения переданных ей аргументов. Вы создадите обобщённую make_object function, которая передаёт аргументы конструктору class, гарантируя, что lvalue копируются, а rvalue перемещаются.
Вы организуете код в трёх файлах:
Widget.h: определите classWidget, отслеживающий способ получения данных.Ваш
Widgetдолжен хранить имя типаstd::stringи значение типаint. Предоставьте два конструктора:- Один принимает
const std::string&(lvalue reference) иint: выводитWidget constructed (copy): [name] - Второй принимает
std::string&&(rvalue reference) иint: выводитWidget constructed (move): [name]и перемещает строку
Также добавьте метод
display(), который выводит[name]: [value].- Один принимает
Factory.h: создайте Factory function с идеальной передачей.Напишите шаблонную function
make_object, которая принимает forwarding reference для имени и обычныйintдля значения. Используйтеstd::forward, чтобы передать аргумент имени конструкторуWidget, сохранив его исходную категорию значения.Function должна возвращать созданный
Widgetпо значению.main.cpp: прочитайте два входных значения:- Имя (строка)
- Значение (целое число)
Продемонстрируйте идеальную передачу, создав два объекта Widget:
- Сначала сохраните имя в локальной переменной и передайте его в
make_objectкак lvalue - Затем передайте временную строку (то же имя, объединённое с
"_temp") напрямую вmake_objectкак rvalue
После создания каждого объекта Widget вызовите его
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 function не знает, получила ли она lvalue или rvalue, но с помощью std::forward сохраняет эту информацию при вызове конструктора Widget. Благодаря этому разрешение перегрузки конструктора выбирает подходящую версию, обеспечивая эффективное перемещение временных объектов и безопасное копирование именованных переменных.
Попробуйте сами
#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
// Сохранить name в локальной переменной и передать её в 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Потренируйтесь самостоятельно: Онлайн-компилятор C++