Вы должны понимать проблему пересылки. Вы можете прочитать всю проблему подробно , но я подведу итоги.
По сути, учитывая выражение 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);
f
будет ли функция, а не выражение?const int i
будет принято:A
выводитсяconst int
. Неудачи для литералов rvalues. Также обратите внимание, что для вызоваdeduced(1)
x этоint&&
неint
(совершенная пересылка никогда не делает копию, как было бы, еслиx
бы был параметр по значению). ПростоT
естьint
. Причина,x
по которой в пересылке вычисляется lvalue, заключается в том, что именованные ссылки rvalue становятся выражениями lvalue.forward
илиmove
здесь? Или это просто семантическая разница?std::move
должен вызываться без явных аргументов шаблона и всегда приводит к значению r, в то время какstd::forward
может заканчиваться как любой. Используйте,std::move
если вы знаете, что вам больше не нужно значение и хотите переместить его в другое место, используйтеstd::forward
для этого в соответствии со значениями, переданными в шаблон функции.Я думаю, что иметь концептуальный код, реализующий std :: forward, можно добавить к обсуждению. Это слайд из выступления Скотта Мейерса «Эффективный пробоотборник C ++ 11/14».
Функция
move
в коде естьstd::move
. Существует (рабочая) реализация для этого ранее в этом разговоре. Я нашел фактическую реализацию std :: forward в libstdc ++ , в файле move.h, но это совсем не поучительно.С точки зрения пользователя, его смысл заключается в том, что
std::forward
это условное приведение к значению. Это может быть полезно, если я пишу функцию, которая ожидает lvalue или rvalue в параметре и хочет передать ее другой функции как rvalue, только если она была передана как rvalue. Если бы я не переносил параметр в std :: forward, он всегда передавался бы как обычная ссылка.Конечно же, он печатает
Код основан на примере из ранее упомянутого разговора. Слайд 10, примерно в 15:00 от начала.
источник
Если вы используете именованную ссылку rvalue в выражении, это фактически lvalue (потому что вы ссылаетесь на объект по имени). Рассмотрим следующий пример:
Теперь, если мы позвоним
outer
такмы бы хотели, чтобы 17 и 29 были перенаправлены на # 2, потому что 17 и 29 являются целочисленными литералами и как таковые. Но так как
t1
иt2
в выраженииinner(t1,t2);
lvalues, вы будете вызывать # 1 вместо # 2. Вот почему мы должны превратить ссылки обратно в неназванные ссылкиstd::forward
. Таким образом,t1
inouter
всегда является выражением lvalue, аforward<T1>(t1)
может быть выражением rvalue в зависимости отT1
. Последнее является только выражением lvalue, еслиT1
является ссылкой lvalue. ИT1
выводится как ссылка lvalue только в том случае, если первый аргумент для external был выражением lvalue.источник
Если после создания экземпляра он
T1
имеет типchar
иT2
класс, вы хотите передать его дляt1
каждой копии и дляt2
каждойconst
ссылки. Ну, если только вы неinner()
берете их за нереференсconst
, то есть в этом случае вы тоже хотите это сделать.Попробуйте написать набор
outer()
функций, которые реализуют это без rvalue ссылок, выводя правильный способ передачи аргументов изinner()
типа. Я думаю, что вам понадобится что-то 2 ^ 2 из них, довольно здоровенные мета-шаблоны, чтобы вывести аргументы, и много времени, чтобы сделать это правильным для всех случаев.И тогда кто-то приходит вместе с
inner()
аргументом на указатель. Я думаю, что сейчас составляет 3 ^ 2. (Или 4 ^ 2. Черт, я не могу попытаться подумать,const
будет ли указатель иметь значение.)А потом представьте, что вы хотите сделать это для пяти параметров. Или семь.
Теперь вы знаете, почему некоторые умники придумали «идеальную пересылку»: это заставляет компилятор делать все это за вас.
источник
Пункт, который не был сделан кристально ясным, - то, что
static_cast<T&&>
обращаетсяconst T&
должным образом также.Программа:
Производит:
Обратите внимание, что 'f' должна быть функцией шаблона. Если это просто определено как 'void f (int && a)', это не сработает.
источник
Возможно, стоит подчеркнуть, что форвард должен использоваться в тандеме с внешним методом с форвардингом / универсальной ссылкой. Допускается использование пересылки в качестве следующих утверждений, но это не приносит пользы, кроме как путаницы. Стандартный комитет может захотеть отключить такую гибкость, иначе почему бы нам просто не использовать static_cast вместо этого?
На мой взгляд, движение вперед и вперед - это шаблоны проектирования, которые являются естественными результатами после введения ссылочного типа r-значения. Мы не должны называть метод, предполагая, что он используется правильно, если только неправильное использование не запрещено.
источник