Menu
Coddy logo textTech

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

Идеальная передача необходима для написания фабричных функций, классов-обёрток и любого обобщённого кода, которому нужно конструировать объекты или вызывать функции, сохраняя намерение вызывающего кода относительно перемещения или копирования.

challenge icon

Задание

Легко

Давайте создадим Factory function, демонстрирующую идеальную передачу, создавая объекты с сохранением категории значения переданных ей аргументов. Вы создадите обобщённую make_object function, которая передаёт аргументы конструктору class, гарантируя, что lvalue копируются, а rvalue перемещаются.

Вы организуете код в трёх файлах:

  • Widget.h: определите class Widget, отслеживающий способ получения данных.

    Ваш 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: прочитайте два входных значения:
    1. Имя (строка)
    2. Значение (целое число)

    Продемонстрируйте идеальную передачу, создав два объекта Widget:

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

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

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

Потренируйтесь самостоятельно: Онлайн-компилятор C++