В идеальной пересылке 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));
}
Вам нужно понять проблему пересылки. Вы можете прочитать всю проблему подробнее, но я подведу итог.
В принципе, учитывая выражение E(a, b, ... , c)
, мы хотим, чтобы выражение f(a, b, ... , 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);
}
Что исправляет вышеуказанную проблему, но flips flops. Теперь он не позволяет 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));
}
Это принимает все значения, может передавать все значения, но потенциально приводит к поведению undefined:
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 комбинаций, кошмар. Мы хотели бы сделать это автоматически.
(Это фактически то, что мы получаем для компилятора для нас в С++ 11.)
В С++ 11 у нас есть шанс исправить это. Одно из решений изменяет правила вычитания шаблонов для существующих типов, но это потенциально может привести к большому количеству кода. Итак, нам нужно найти другой способ.
Решение состоит в том, чтобы вместо этого использовать вновь добавленные rvalue-ссылки; мы можем ввести новые правила при выводе типов rvalue-reference и создать любой желаемый результат. В конце концов, мы не можем сломать код сейчас.
Если задана ссылка на ссылку (ссылка на примечание - это охватывающий термин, означающий как T&
, так и T&&
), мы используем следующее правило для определения результирующего типа:
"[данный] тип TR, который является ссылкой на тип T, попытка создать тип" lvalue reference to cv TR "создает тип" lvalue reference to 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&&)
Последнее означает "переслать" категорию значений переменной. Имейте в виду, что когда-то внутри функции параметр может быть передан как значение l на что-либо:
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
получает значение r. Совершенная.
И, конечно, мы хотим избавиться от уродливых. static_cast<T&&>
загадочно и странно запомнить; вместо этого сделайте служебную функцию с именем forward
, которая делает то же самое:
std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
Я думаю, что концептуальный код, реализующий std :: forward, может добавить к обсуждению. Это слайд со слова Скотта Мейерса "Эффективный С++ 11/14 Sampler"
Функция 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 с самого начала.
В идеальной пересылке 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 являются целыми литералами и как таковые значения r. Но так как t1
и t2
в выражении inner(t1,t2);
являются lvalues, вы будете вызывать # 1 вместо # 2. Поэтому нам нужно вернуть ссылки в неназванные ссылки с помощью std::forward
. Таким образом, t1
в outer
всегда является выражением lvalue, а forward<T1>(t1)
может быть выражением rvalue в зависимости от t1
. Последний является только выражением lvalue, если t1
является ссылкой на lvalue. И t1
выводится только как ссылка lvalue в случае, если первый аргумент для внешнего является выражением lvalue.
Как это повлияет на вызываемую функцию inner, если оставить t1 и t2 как 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&
.
Программа:
#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), это не работает.
Возможно, стоит подчеркнуть, что forward необходимо использовать в тандеме с внешним методом с пересылкой/универсальной ссылкой. Использование пересылки само по себе, поскольку допускаются следующие утверждения, но не делает ничего хорошего, кроме как путаницы. Стандартный комитет может захотеть отключить такую гибкость, иначе почему бы нам просто не использовать static_cast?
std::forward<int>(1);
std::forward<std::string>("Hello");
По моему мнению, перемещение и форвард - это шаблоны проектирования, которые являются естественными результатами после введения ссылочного типа r-value. Мы не должны называть метод, предполагающий его правильное использование, если неправильное использование запрещено.
f
, а не выражение?f
может быть чем-то «вызываемым»; функция, указатель функции или объект функции.