Каковы основные цели использования std :: forward и какие проблемы он решает?

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

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

person Steveng    schedule 27.08.2010    source источник


Ответы (6)


Вы должны понимать проблему пересылки. Вы можете подробно прочитать всю проблему, но я резюмирую.

По сути, учитывая выражение E(a, b, ... , c), мы хотим, чтобы выражение f(a, b, ... , c) было эквивалентным. В C ++ 03 это невозможно. Есть много попыток, но все они в каком-то смысле терпят неудачу.


Самый простой - использовать lvalue-ссылку:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

Но это не может обрабатывать временные значения: f(1, 2, 3);, поскольку они не могут быть привязаны к lvalue-ссылке.

Следующая попытка может быть:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

Что решает указанную выше проблему, но шлепает. Теперь он не может позволить E иметь неконстантные аргументы:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these

Третья попытка принимает константные ссылки, но затем const_cast const прочь:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

Это принимает все значения, может передавать все значения, но потенциально приводит к неопределенному поведению:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!

Окончательное решение обрабатывает все правильно ... ценой невозможности обслуживания. Вы предоставляете перегрузки f с всеми комбинациями констант и неконстант:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N аргументов требует 2 N комбинаций, кошмар. Мы бы хотели сделать это автоматически.

(Это фактически то, что мы заставляем компилятор делать за нас в C ++ 11.)


В C ++ 11 у нас есть шанс исправить это. Одно решение изменяет правила вывода шаблонов для существующих типов, но это потенциально нарушает большой объем кода. Поэтому нам нужно найти другой способ.

Решение состоит в том, чтобы вместо этого использовать недавно добавленные rvalue-ссылки; мы можем ввести новые правила при выводе типов rvalue-reference и создать любой желаемый результат. В конце концов, мы не можем сейчас взломать код.

Если дана ссылка на ссылку (ссылка на заметку - это охватывающий термин, означающий как T&, так и T&&), мы используем следующее правило, чтобы определить результирующий тип:

"[учитывая] тип TR, который является ссылкой на тип T, попытка создать тип« ссылка lvalue на cv TR »создает тип« ссылка lvalue на T », в то время как попытка создать тип« ссылка rvalue на cv TR »создает тип TR».

Или в табличной форме:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

Затем с выводом аргумента шаблона: если аргумент является lvalue A, мы предоставляем аргументу шаблона ссылку lvalue на A. В противном случае мы делаем вывод обычно. Это дает так называемые универсальные ссылки (термин ссылка на переадресацию теперь является официальной).

Почему это полезно? Поскольку вместе мы сохраняем возможность отслеживать категорию значений типа: если это было lvalue, у нас есть параметр lvalue-reference, в противном случае у нас есть параметр rvalue-reference.

В коде:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

Последнее, что нужно сделать - «переместить» категорию значения переменной. Имейте в виду, что, оказавшись внутри функции, параметр может быть передан как lvalue чему угодно:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

Это не хорошо. Е необходимо получить такую ​​же ценностную категорию, как и у нас! Решение такое:

static_cast<T&&>(x);

Что это значит? Предположим, мы внутри функции deduce, и нам передано lvalue. Это означает, что T - это A&, и поэтому целевой тип для статического приведения - A& && или просто A&. Поскольку x уже является A&, мы ничего не делаем, и нам остается ссылка на lvalue.

Когда нам было передано rvalue, T = A, поэтому целевой тип для статического приведения - A&&. Приведение приводит к выражению rvalue, которое больше не может быть передано в ссылку lvalue. Мы сохранили категорию значения параметра.

Их объединение дает нам "идеальную пересылку":

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

Когда f получает lvalue, E получает lvalue. Когда f получает rvalue, E получает rvalue. Идеально.


И, конечно же, мы хотим избавиться от уродливого. static_cast<T&&> загадочно и странно запоминать; давайте вместо этого создадим служебную функцию с именем forward, которая делает то же самое:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
person GManNickG    schedule 27.08.2010
comment
Разве f не будет функцией, а не выражением? - person Michael Foukarakis; 27.08.2010
comment
@sbi: Ха-ха. :) ‹3 @mfukar: Это выражение вызова функции. f сам может быть чем угодно вызываемым; функция, указатель на функцию или объект функции. - person GManNickG; 27.08.2010
comment
Спасибо за исчерпывающий ответ. Просто вопрос, в выводе (1) x - это тип int && или int? Потому что я думаю, что где-то читал, что если это тип rvalue, T будет преобразован в int. Таким образом, с дополнительным && он станет int && type. Может кто-нибудь прояснить это? thbecker.net/articles/rvalue_references/section_08.html - person Steveng; 27.08.2010
comment
Ваша последняя попытка неверна в отношении постановки проблемы: она будет пересылать значения const как неконстантные, поэтому пересылка не выполняется вообще. Также обратите внимание, что при первой попытке const int i будет принят: A будет выведен на const int. Ошибки относятся к литералам rvalues. Также обратите внимание, что для вызова deduced(1) x равно int&&, а не int (при идеальной пересылке копия не создается, как это было бы, если бы x был параметром по значению). Просто T это int. Причина, по которой x оценивается как lvalue в сервере пересылки, состоит в том, что именованные ссылки rvalue становятся выражениями lvalue. - person Johannes Schaub - litb; 27.08.2010
comment
При использовании таксономии выражений C ++ 0x: x оценивается как lvalue в перенаправителе, а static_cast<T&&>(x) оценивается как xvalue в перенаправителе. Lvalue и xvalue называются glvalues ​​. Prvalues ​​ (литералы и т. д.) и xvalues ​​называются rvalues ​​. - person Johannes Schaub - litb; 27.08.2010
comment
Извините за медленный ответ. @ Стивенг: Да, я был неправ, как объяснил Йоханнес. @Johannes: Спасибо за исправления (и, черт возьми, с моей стороны, что A в первой части было выведено как const int, я просто не думал.) Признаюсь, я даже не читал новую таксономию или правила дедукции. : S (Только что, неплохо.) Это правильно? - person GManNickG; 28.08.2010
comment
Ответ положительно выдающийся. Если бы я мог проголосовать за него несколько раз, я бы это сделал, и спасибо @Johannes за исправления. Это честно принадлежит как кандидат вики. - person WhozCraig; 30.03.2013
comment
Есть ли здесь разница в использовании forward или move? Или это просто смысловая разница? - person 0x499602D2; 01.04.2013
comment
@David: std::move должен вызываться без явных аргументов шаблона и всегда приводит к значению r, тогда как std::forward может иметь любое значение. Используйте std::move, если вы знаете, что вам больше не нужно значение и хотите переместить его в другое место, используйте std::forward, чтобы сделать это в соответствии со значениями, переданными в ваш шаблон функции. - person GManNickG; 01.04.2013
comment
Спасибо, что начали с конкретных примеров и мотивировали проблему; очень полезно! - person ShreevatsaR; 30.01.2015
comment
Это выдающийся ответ. Меня также радует, что Python просто использует вызов по объекту, и грустно, что мне приходится использовать C ++. - person Claudiu; 12.05.2015
comment
Хорошее объяснение. Только что наткнулся на эту статью, которая дает хорошее объяснение thbecker.net/articles/rvalue_references/section_01.html < / а> - person Dhaval; 19.02.2016
comment
нет необходимости в других ответах, это то, что Скотт Мейерс представил в своей книге и был красиво и подробно представлен в этом ответе. Превосходно - person Blood-HaZaRd; 18.02.2019
comment
Где написано: «Когда нам передали rvalue, T - A - почему?» Почему T не A &&? Заявленное обоснование для std :: forward () похоже на это, но я не понимаю, почему компилятор выводит T как A, когда он может легко вывести его как A && (тем самым устраняя необходимость в std :: вперед()). - person Ziffusion; 10.03.2019
comment
@Ziffusion: Прошло достаточно времени, и теперь я не уверен в деталях, но IIRC это сломало бы существующий (до C ++ 11) код. Я не могу привести очевидный пример, может template <typename T> void check(const T&)? - person GManNickG; 12.03.2019
comment
Базовая реализация как static_cast<A&&>(a) рассматривается на open- std.org/jtc1/sc22/wg21/docs/papers/2009/n2951.html как для случая 0, но не проходит два из 6 тестов. - person user2023370; 23.08.2019
comment
@GManNickG Что означает выражение E (a, b, ..., c), мы хотим, чтобы выражение f (a, b, ..., c) было эквивалентным. иметь в виду? Как я могу это понять? - person John; 13.06.2020
comment
@John: Это означает, что запись f(a, b, ..., c) ведет себя так, как если бы я написал E(a, b, ..., c) для всех возможных аргументов. В первом примере это требование не выполняется, потому что f(1, 2, 3) не работает, а E(1, 2, 3) работает. Связанный документ более формальный. - person GManNickG; 13.06.2020
comment
@GManNickG Спасибо за разъяснения. Еще один вопрос, std::forward генерирует сборку? - person John; 14.06.2020
comment
@John Он изменяет только категорию значений аргументов, поэтому я ожидаю, что в оптимизированной сборке сборка не производится. Однако только вы можете дизассемблировать свою программу, чтобы это выяснить. - person GManNickG; 14.06.2020
comment
@GManNickG Еще один вопрос: какой оператор подходит для вывода int a;f(a); :, поскольку a - это lvalue, поэтому int(T&&) приравнивается к int(int& &&) или чтобы T&& равнялось int&, поэтому T должно быть int&? Я предпочитаю последнее. Я смущен тем, что является причиной, а какое результатом. - person John; 15.06.2020
comment
@John Из примера в ответе: deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&). Поскольку a - это lvalue, T будет int&. Это независимо от параметров. Тот факт, что тип параметра тогда T&& - ›T& && -› T&, - выбор, сделанный разработчиком при добавлении && в шаблон типа. - person GManNickG; 15.06.2020
comment
Привет, @GManNickG, спасибо за ответ! Но меня смущает последний okay: deduce(1); // okay, foo operates on x which has a value of 1, который, на мой взгляд, не подходит, поскольку тип аргумента foo равен int&, но при выводе типа x это int&&. А под этой строкой вы публикуете That's no good, так что же здесь конкретно означают okay и no good? Благодарю. - person roachsinai; 05.01.2021

Я думаю, что концептуальный код, реализующий std :: forward, может помочь в понимании. Это слайд из выступления Скотта Мейерса Эффективный C + +11/14 Sampler

концептуальный код, реализующий std :: forward

Функция move в коде - std::move. Ранее в этом выступлении для него была представлена ​​(рабочая) реализация. Я нашел фактическую реализацию std :: forward в libstdc ++, в файле move.h, но это совсем не поучительно.

С точки зрения пользователя это означает, что std::forward является условным приведением к rvalue. Это может быть полезно, если я пишу функцию, которая ожидает в параметре либо lvalue, либо rvalue, и хочет передать его другой функции как rvalue, только если она была передана как rvalue. Если бы я не заключил параметр в std :: forward, он всегда передавался бы как обычная ссылка.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Конечно же, он печатает

std::string& version
std::string&& version

Код основан на примере из ранее упомянутого выступления. Слайд 10, примерно в 15:00 с начала.

person user7610    schedule 01.05.2014
comment
Ваша вторая ссылка в конечном итоге указывает на совершенно другое место. - person Pharap; 04.05.2018
comment
Вау, отличное объяснение. Я начал с этого видео: youtube.com/watch?v=srdwFMZY3Hg, но после читая твой ответ, наконец-то я это чувствую. :) - person flamingo; 16.11.2020

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

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

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

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

Теперь, если мы назовем outer вот так

outer(17,29);

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

person sellibitze    schedule 29.08.2010
comment
Это своего рода смягченное объяснение, но очень хорошо сделанное и функциональное объяснение. Люди должны сначала прочитать этот ответ, а затем при желании углубиться - person NicoBerrogorry; 28.06.2018
comment
@sellibitze Еще один вопрос, какое утверждение является правильным при выводе int a; f (a): поскольку a является lvalue, поэтому int (T &&) приравнивается к int (int & &&) или чтобы T && приравнивался к int &, поэтому T должен быть int &? Я предпочитаю последний. - person John; 14.06.2020

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

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

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

И затем кто-то приходит с inner(), который принимает аргументы для каждого указателя. Я думаю, что теперь это 3 ^ 2. (Или 4 ^ 2. Черт, я не хочу думать, будет ли указатель const иметь значение.)

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

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

person sbi    schedule 27.08.2010

Не совсем ясно, что 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&&

Обратите внимание, что «f» должна быть функцией-шаблоном. Если он просто определен как void f (int && a), это не сработает.

person Bill Chapman    schedule 23.02.2016
comment
хороший момент, так что T && в статическом приведении также следует правилам свертывания ссылок, верно? - person barney; 16.08.2016

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

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

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

person colin    schedule 07.12.2017
comment
Я не думаю, что комитет по C ++ считает, что на них лежит ответственность за правильное использование языковых идиом или даже за определение того, что такое правильное использование (хотя они, безусловно, могут дать рекомендации). С этой целью, хотя учителя, начальники и друзья могут быть обязаны направлять их так или иначе, я считаю, что комитет C ++ (и, следовательно, стандарт) не имеет этой обязанности. - person SirGuy; 15.12.2017
comment
Да, я только что прочитал N2951 и я согласен с тем, что комитет по стандартизации не обязан добавлять ненужные ограничения в отношении использования функции. Но имена этих двух шаблонов функций («перемещение» и «вперед») действительно немного сбивают с толку, поскольку в файле библиотеки или стандартной документации присутствуют только их определения (23.2.5 помощники «Пересылка / перемещение»). Примеры в стандарте определенно помогают понять концепцию, но может быть полезно добавить больше замечаний, чтобы сделать вещи немного более ясными. - person colin; 20.12.2017