Как это повлияет на вызываемую внутреннюю функцию, если мы оставим t1 и t2 как lvalue?
Если после создания экземпляра T1
имеет тип char
и T2
относится к классу, вы хотите передать t1
на копию и t2
на ссылку const
. Ну, если только inner()
не берет их по не-const
ссылке, то есть в этом случае вы тоже хотите это сделать.
Попробуйте написать набор функций outer()
, которые реализуют это без ссылок на rvalue, выводя правильный способ передачи аргументов из типа inner()
. Я думаю, вам понадобится что-то 2 ^ 2 из них, довольно здоровенный материал шаблона мета, чтобы вывести аргументы, и много времени, чтобы сделать это правильно для всех случаев.
А затем кто-то приходит с inner()
, который принимает аргументы для каждого указателя. Я думаю, что теперь это составляет 3^2. (Или 4 ^ 2. Черт, я даже не пытаюсь думать, будет ли иметь значение указатель const
.)
А затем представьте, что вы хотите сделать это для пяти параметров. Или семь.
Теперь вы знаете, почему некоторые светлые умы придумали "идеальную переадресацию": она заставляет компилятор делать все это за вас.
При идеальной пересылке std::forward используется для преобразования именованной ссылки rvalue t1 и t2 в безымянную ссылку rvalue. Какова цель этого? Как это повлияет на внутреннюю функцию, если мы оставим t1 и t2 как lvalue?
template
void external(T1&& t1, T2&& t2) { внутренний(std::forward (t1), std::forward (t2)); }
Если вы используете именованную ссылку rvalue в выражении, это фактически lvalue (поскольку вы обращаетесь к объекту по имени). Рассмотрим следующий пример:
void inner(int &, int &); // #1
void inner(int &&, int &&); // #2
Теперь, если мы вызовем outer
следующим образом
outer(17,29);
, мы хотели бы, чтобы 17 и 29 были перенаправлены на #2, потому что 17 и 29 являются целочисленными литералами и, как таковые, rvalue. Но поскольку t1
и t2
в выражении inner(t1,t2);
являются значениями lvalue, вы будете вызывать #1 вместо #2. Вот почему нам нужно превратить ссылки обратно в безымянные ссылки с помощью std::forward
. Итак, t1
в outer
всегда является выражением lvalue, тогда как forward
может быть выражением rvalue в зависимости от T1
. Последнее является выражением lvalue только в том случае, если T1
является ссылкой lvalue. И T1
выводится как ссылка lvalue только в том случае, если первый аргумент external был выражением lvalue.
Вы должны понимать проблему переадресации. Вы можете прочитать всю задачу подробно, но я подытожу.
В принципе, учитывая выражение 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 аргументов требуют 2N комбинаций, кошмар. Мы хотели бы сделать это автоматически.
(По сути, это то, что компилятор делает за нас в C++11.)
В C++11 у нас есть шанс это исправить. Одно из решений изменяет правила вывода шаблонов для существующих типов, но это может нарушить большую часть кода. Итак, мы должны найти другой путь.
Решение состоит в том, чтобы вместо этого использовать только что добавленные rvalue-ссылки; мы можем ввести новые правила при выводе типов rvalue-reference и создать любой желаемый результат. В конце концов, сейчас мы не можем взломать код.
Если дана ссылка на ссылку (ссылка на примечание — это общий термин, означающий как T&
, так и T&&
), мы используем следующее правило для определения результирующего типа:
«[дан] тип TR, который является ссылкой на тип T, попытка создать тип «ссылка lvalue на cv TR» создает тип «ссылка lvalue на T», а попытка создать тип «ссылка rvalue to 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.
Когда нам передается значение 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
является загадочным и странным для запоминания; давайте вместо этого создадим вспомогательную функцию с именем forward
, которая делает то же самое:
std::forward<A>(a);
// is the same as
static_cast<A&&>(a);