5

Основные цели std::forward и решаемые им проблемы

5

Описание проблемы:

Я изучаю концепцию идеальной передачи (perfect forwarding) в C++ и столкнулся с вопросом о том, как работает функция std::forward. В приведенном ниже коде std::forward используется для преобразования именованных rvalue-ссылок t1 и t2 в безымянные rvalue-ссылки:

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Какова цель этого преобразования? Как это повлияет на вызываемую функцию inner, если оставить t1 и t2 в виде lvalue-ссылок?

Дополнительно, интересно, как это изменение типов параметров влияет на семантику передачи значений. Буду признателен за подробные объяснения и примеры.

5 ответ(ов)

0

В perfect forwarding функция std::forward используется для преобразования именованных rvalue-ссылок t1 и t2 в безымянные rvalue-ссылки. Какова цель такого преобразования? Как это повлияет на вызываемую функцию inner, если оставить t1 и t2 как lvalue?

Дело в том, что при использовании именованной rvalue-ссылки в выражении она на самом деле считается lvalue (поскольку вы обращаетесь к объекту по имени). Рассмотрим следующий пример:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Теперь, если мы вызовем outer следующим образом:

outer(17, 29);

мы хотим, чтобы 17 и 29 были переданы в #2, так как 17 и 29 являются литералами типа int и, следовательно, rvalue. Однако поскольку t1 и t2 в выражении inner(t1, t2); являются lvalue, то мы вызовем #1 вместо #2. Именно поэтому нам нужно вернуть ссылки обратно в безымянные ссылки с помощью std::forward. Таким образом, t1 в outer всегда является lvalue-выражением, в то время как std::forward<T1>(t1) может быть rvalue в зависимости от T1. Последнее является lvalue-выражением только в том случае, если T1 - это lvalue-ссылка. И T1 будет выведено как lvalue-ссылка только в том случае, если первый аргумент в outer был lvalue-выражением.

0

Если после инстанцирования T1 будет иметь тип char, а T2 будет классом, и вы хотите передать t1 по значению, а t2 по const ссылке, то это вполне приемлемо, если функция inner() не принимает их по неконстантной ссылке. В противном случае, если inner() принимает параметры именно так, то вам также следует делать то же самое.

Попробуйте написать набор функций outer(), которые реализуют это без rvalue-ссылок, выведя правильный способ передачи аргументов в зависимости от типов inner(). Я думаю, вам потребуется около 2^2 вариаций, что потребует довольно сложной темплейтной метапрограммирования для корректного определения аргументов и много времени, чтобы всё это правильно настроить для всех случаев.

А потом кто-то приходит с функцией inner(), которая принимает аргументы по указателю. Теперь у нас получается 32 (или 42. Честно говоря, мне лень считать, повлияет ли const указатель на итог).

А теперь представьте, что вы хотите сделать это для пяти параметров. Или семи.

Теперь вы понимаете, почему некоторые умные умы придумали "идеальную переадресацию": это позволяет компилятору выполнять всю эту работу за вас.

0

Важный момент, который не был четко обозначен, заключается в том, что static_cast<T&&> корректно обрабатывает также и const T&.

Рассмотрим приведённый вами код:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Результат выполнения программы будет:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Как видно из вывода, функция g вызывается с нужными аргументами в зависимости от переданного типа. Шаблонная функция f обрабатывает аргументы таким образом, что при наличии rvalue (например, f(1) или f(a * b)) передается int&&, а при передаче lvalue (например, f(a)) — int&.

Стоит отметить, что static_cast<T&&>(a) в данном контексте корректно работает и с const T&. Например, когда мы вызываем f(b) (где b имеет тип const int), T будет выводиться как const int, и static_cast<T&&>(a) приведет к const int&, что затем правильно направляется в функцию g, которая обрабатывает этот конкретный случай.

Если бы функция f была просто определена как void f(int&& a), это бы не сработало для const int&, поскольку такой вариант не смог бы использовать универсальную ссылку, что ограничивало бы ее применение только для rvalue.

0

Стоит подчеркнуть, что использование std::forward должно сочетаться с внешним методом, который поддерживает перемещения/универсальные ссылки. Использовать std::forward самостоятельно, как в приведенных примерах, допустимо, но это не приносит никакой пользы, кроме как создавать путаницу. Возможно, комитет по стандартам должен рассмотреть возможность ограничения такой гибкости — иначе, почему бы просто не использовать static_cast вместо этого?

std::forward<int>(1);
std::forward<std::string>("Hello");

На мой взгляд, std::move и std::forward являются паттернами проектирования, которые естественным образом возникли после введения типа ссылки на r-значения. Мы не должны называть метод, предполагая, что он будет правильно использован, если неправильное использование не запрещено.

0

В вашем вопросе вы затрагиваете тему передачи rvalue и lvalue ссылок при использовании универсальных ссылок в C++. Давайте рассмотрим подробнее, как это работает на примерах, которые вы привели.

Когда вы пишете:

auto&& x = 2; // x имеет тип int&&

x становится обобщенной ссылкой (universal reference), и тип переменной будет int&&, так как вы инициализируете ее временным значением.

Следующим шагом:

auto&& y = x; // Но y это int&

Здесь y принимает тип int&, потому что x - это lvalue-ссылка (ссылка на lvalue).

Теперь обратите внимание на:

auto&& z = std::forward<decltype(x)>(x); // z это int&&

С помощью std::forward, мы сохранили тип z таким же, как у x - int&&. Это важно, когда вы хотите сохранить семантику переменной (в данном случае rvalue-ссылки).

Если мы говорим о lvalue-ссылках, то можно посмотреть на следующий пример:

int i;

auto&& x = i; // x это int&
auto&& y = x; // y это int&
auto&& z = std::forward<decltype(x)>(x); // z это int&

В этом случае все ссылки (x, y, z) будут иметь тип int&, как мы и ожидали, поскольку i - это lvalue.

Теперь, если у вас есть функция с перегрузками, которая принимает int& и int&&, и вы хотите передать переменные, подобные z, а не y, использование std::forward является правильным способом, чтобы обеспечить корректное разрешение перегрузок.

Чтобы проверить типы переменных в вашем примере, вы можете использовать std::is_same_v следующим образом:

std::cout << std::is_same_v<int&, decltype(z)>; // Это вернет true
std::cout << std::is_same_v<int&&, decltype(z)>; // Это вернет false

Если у вас остались вопросы или нужны дополнительные примеры, не стесняйтесь спрашивать!

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь