Menu
Coddy logo textTech

Идеальная передача (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) необходима для написания фабричных функций, классов-оберток и любого обобщенного кода, которому требуется конструировать объекты или вызывать функции, сохраняя при этом намерение вызывающей стороны в отношении перемещения или копирования.

challenge icon

Задание

Легко

Давайте создадим функцию-фабрику, которая демонстрирует идеальную пересылку (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: Считайте два входных значения:
    1. Имя (строка)
    2. Значение (целое число)

    Продемонстрируйте идеальную пересылку, создав два виджета:

    1. Сначала сохраните имя в локальной переменной и передайте его в make_object как lvalue
    2. Затем передайте временную строку (то же имя с добавлением "_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;
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование