Преимущества использования форварда - PullRequest
395 голосов
/ 27 августа 2010

При совершенной пересылке 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));
}

Ответы [ 6 ]

729 голосов
/ 27 августа 2010

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

По сути, учитывая выражение 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-reference.

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

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-reference ; мы можем ввести новые правила при выводе 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

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

static_cast<T&&>(x);

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

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

Соединяя их вместе, мы получим "идеальную пересылку":

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

Когда f получает lvalue, E получает lvalue. Когда f получает значение r, E получает значение r. Идеально подходит.


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

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
51 голосов
/ 01 мая 2014

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

conceptual code implementing std::forward

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

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

26 голосов
/ 30 августа 2010

При совершенной пересылке 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.Таким образом, t1 в outer всегда является выражением lvalue, в то время как forward<T1>(t1) может быть выражением rvalue в зависимости от T1.Последнее является только выражением lvalue, если T1 является ссылкой lvalue.И T1 выводится как ссылка lvalue только в том случае, если первый аргумент для external был выражением lvalue.

11 голосов
/ 27 августа 2010

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

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

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

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

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

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

4 голосов
/ 23 февраля 2016

Ситуация, которая не была прояснена, заключается в том, что 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)', это не сработает.

2 голосов
/ 07 декабря 2017

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

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

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

...