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


438

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

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

Ответы:


789

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

По сути, учитывая выражение 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-ссылки, но потом 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со всеми комбинациями const и non-const:

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 ссылка на тип T » 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

Это не хорошо. E должен получить такую ​​же категорию ценностей, что и мы! Решение заключается в следующем:

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);

1
Не fбудет ли функция, а не выражение?
Майкл Фукаракис

1
Ваша последняя попытка неверна в отношении формулировки проблемы: она будет пересылать значения const как неконстантные, таким образом, вообще не пересылать. Также обратите внимание, что в первой попытке const int iбудет принято: Aвыводится const int. Неудачи для литералов rvalues. Также обратите внимание, что для вызова deduced(1)x это int&&не int(совершенная пересылка никогда не делает копию, как было бы, если xбы был параметр по значению). Просто Tесть int. Причина, xпо которой в пересылке вычисляется lvalue, заключается в том, что именованные ссылки rvalue становятся выражениями lvalue.
Йоханнес Шауб -

5
Есть ли разница в использовании forwardили moveздесь? Или это просто семантическая разница?
0x499602D2

28
@David: std::moveдолжен вызываться без явных аргументов шаблона и всегда приводит к значению r, в то время как std::forwardможет заканчиваться как любой. Используйте, std::moveесли вы знаете, что вам больше не нужно значение и хотите переместить его в другое место, используйте std::forwardдля этого в соответствии со значениями, переданными в шаблон функции.
GManNickG

5
Спасибо, что сначала начали с конкретных примеров и мотивировали проблему; очень полезно!
ShreevatsaR

61

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

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

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

С точки зрения пользователя, его смысл заключается в том, что std::forwardэто условное приведение к значению. Это может быть полезно, если я пишу функцию, которая ожидает 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 от начала.


2
Ваша вторая ссылка оказалась в другом месте.
Pharap

34

В идеальной пересылке std :: forward используется для преобразования именованной ссылки rvalue t1 и t2 в безымянную ссылку rvalue. Какова цель сделать это? Как это повлияет на вызываемую функцию inner, если мы оставим 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 являются целочисленными литералами и как таковые. Но так как t1и t2в выражении inner(t1,t2);lvalues, вы будете вызывать # 1 вместо # 2. Вот почему мы должны превратить ссылки обратно в неназванные ссылки std::forward. Таким образом, t1in outerвсегда является выражением lvalue, а forward<T1>(t1)может быть выражением rvalue в зависимости от T1. Последнее является только выражением lvalue, если T1является ссылкой lvalue. И T1выводится как ссылка lvalue только в том случае, если первый аргумент для external был выражением lvalue.


Это своего рода смягченное объяснение, но очень хорошо сделанное и функциональное объяснение. Люди должны сначала прочитать этот ответ, а затем пойти глубже, если это необходимо
NicoBerrogorry

@sellibitze Еще один вопрос, какое утверждение верно при выводе int a; f (a): «поскольку a является l-значением, то int (T &&) приравнивается к int (int & &&)» или «, чтобы сделать T && равным int &, так что T должно быть int & "? Я предпочитаю последний.
Джон

11

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

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

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

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

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

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


5

Пункт, который не был сделан кристально ясным, - то, что 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)', это не сработает.


Хороший вопрос, поэтому T && в статическом приведении также следует правилам свертывания ссылок, верно?
Барни

3

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

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

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


Я не думаю, что комитет по С ++ считает, что они обязаны «правильно» использовать языковые идиомы и даже не определять, что такое «правильное» использование (хотя они, безусловно, могут дать рекомендации). В связи с этим, хотя у учителей, начальников и друзей человека может быть обязанность руководить ими так или иначе, я считаю, что комитет C ++ (и, следовательно, стандарт) не имеет такой обязанности.
SirGuy

Да, я только что прочитал N2951, и я согласен, что стандартный комитет не обязан добавлять ненужные ограничения в отношении использования функции. Но имена этих двух шаблонов функций (перемещение и пересылка) действительно немного сбивают с толку, видя только их определения в файле библиотеки или стандартной документации (23.2.5 Помощники пересылки / перемещения). Примеры в стандарте определенно помогают понять концепцию, но может быть полезно добавить больше замечаний, чтобы сделать вещи немного более понятными.
Colin
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.