Преимущества использования форварда
В идеальной пересылке, std::forward
используется для преобразования именованных ссылок rvalue t1
а также t2
на неназванные ссылки. Какова цель сделать это? Как это повлияет на вызываемую функцию? inner
если мы уйдем t1
& t2
как значения?
template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2)
{
inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
7 ответов
Вы должны понимать проблему пересылки. Вы можете прочитать всю проблему подробно, но я подведу итоги.
В основном, учитывая выражение 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 аргументов требуют 2N комбинаций, кошмар. Мы хотели бы сделать это автоматически.
(Это эффективно то, что мы заставляем компилятор делать для нас в 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
получает значение когда f
получает значение, E
получает значение. Отлично.
И конечно, мы хотим избавиться от безобразного. static_cast<T&&>
загадочно и странно запоминать; давайте вместо этого сделаем вспомогательную функцию под названием forward
, который делает то же самое:
std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
Я думаю, что иметь концептуальный код, реализующий std::forward, можно добавить к обсуждению. Это слайд из выступления Скотта Мейерса "Эффективный пробоотборник C++11/14".
функция 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 от начала.
В идеальной пересылке 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.
Как это повлияет на вызываемую функцию 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)', это не сработает.
Возможно, стоит подчеркнуть, что форвард должен использоваться в тандеме с внешним методом с форвардингом / универсальной ссылкой. Допускается использование пересылки в качестве следующих утверждений, но это не приносит пользы, кроме как путаницы. Стандартный комитет может захотеть отключить такую гибкость, иначе почему бы нам просто не использовать static_cast вместо этого?
std::forward<int>(1);
std::forward<std::string>("Hello");
По моему мнению, движение вперед и вперед - это шаблоны проектирования, которые являются естественными результатами после введения ссылочного типа r-значения. Мы не должны называть метод, предполагая, что он используется правильно, если только неправильное использование не запрещено.
From another viewpoint, when dealing with rvalues in a universal reference assignment, it may be desirable to preserve the type of a variable as it is. For example
auto&& x = 2; // x is int&&
auto&& y = x; // But y is int&
auto&& z = std::forward<decltype(x)>(x); // z is int&&
Using , we ensured exactly has the same type as .
Moreover,
std::forward
doesn't affect lvalue references:
int i;
auto&& x = i; // x is int&
auto&& y = x; // y is int&
auto&& z = std::forward<decltype(x)>(x); // z is int&
Still has the same type as
x
.
So, back to your case, if the inner function has two overloads for
int&
and
int&&
, you want to pass variables like
z
assignment not
y
one.
The types in the example can be assessed via:
std::cout<<is_same_v<int&,decltype(z)>;
std::cout<<is_same_v<int&&,decltype(z)>;