2010-08-27 7 views
324

В идеальной пересылке std::forward используется для преобразования названных ссылок raleue 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)); 
} 

ответ

11

Как это повлияет на вызываемой функции внутренней, если оставить t1 t2 & в Lvalue?

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

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

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

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

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

607

Вы должны понимать проблему пересылки. Вы можете read the entire problem in detail, но я подведу итог.

В принципе, учитывая выражение 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 «S 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 аргументы требуют 2 N комбинации, кошмар. Мы хотели бы сделать это автоматически.

(Это фактически то, что мы получаем компилятор сделать для нас в C++ 11.)


В C++ 11, мы получаем шанс, чтобы исправить это. One solution modifies template deduction rules on existing types, but this potentially breaks a great deal of code. Итак, нам нужно найти другой путь.

Решение заключается в том, чтобы вместо этого использовать вновь добавленные rvalue-ссылки; мы можем ввести новые правила при выводе типов rvalue-reference и создать любой желаемый результат. В конце концов, мы не сможем сломать код сейчас.

Если дается ссылка на ссылки (ссылка записка представляет собой всеобъемлющий термин, означающий как T& и T&&), мы используем следующее правило, чтобы выяснить, результирующий тип:

«[дано] тип ТР это ссылка на тип T, попытка создания типа «ссылка lvalue на cv TR» создает тип «lvalue reference to T», в то время как попытка создать тип «rvalue reference 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) 

Далее, с аргументом шаблона дедукцией: если аргумент именующего A, мы поставляем аргумент шаблона с Lvalue ссылкой на А. В противном случае, мы выводим нормально , Это дает так называемые универсальные ссылки (теперь официальный номер forwarding reference).

Почему это полезно? Поскольку в сочетании мы сохраняем способность отслеживать категорию значений типа: если это значение 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 получает именующее выражение, E получает именующее выражение. Когда f получает rvalue, E получает rvalue. Отлично.


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

std::forward<A>(a); 
// is the same as 
static_cast<A&&>(a); 
+0

Не будет ли 'f' быть функцией, а не выражением? –

+0

@sbi: Ха-ха. :) <3 @mfukar: Это выражение вызова функции. 'f' может быть любым« вызываемым »; функция, указатель функции или объект функции. – GManNickG

+0

Спасибо за ваш полный ответ. Просто вопрос, в выводе (1), x есть тип int && или int? Поскольку я думаю, что я где-то читал, что если это тип rvalue, T будет разрешен для int. Таким образом, с дополнительным &&, он станет int && type. Может ли кто-нибудь прояснить это? http://thbecker.net/articles/rvalue_references/section_08.html – Steveng

13

В идеальном экспедировании, станд :: вперед используются для преобразования имени 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 в выражении это на самом деле именующее (потому что вы обращаетесь к объекту по имени). Рассмотрим следующий пример:

void inner(int &, int &); // #1 
void inner(int &&, int &&); // #2 

Теперь, если мы называем outer как этот

outer(17,29); 

мы хотели бы 17 и 29, которые будут направлены на # 2, так как 17 и 29 являются целыми литералы и как такие rvalues , Но так как t1 и t2 в выражении inner(t1,t2); являются lvalues, вы будете вызывать # 1 вместо # 2. Вот почему нам нужно вернуть ссылки в неназванные ссылки с помощью std::forward. Таким образом, t1 в outer всегда является выражением lvalue, а forward<T1>(t1) может быть выражением rvalue в зависимости от T1. Последний является только выражением lvalue, если T1 является ссылкой на lvalue. И T1 выводится только как ссылка lvalue в случае, если первым аргументом для внешнего является выражение lvalue.

+0

И, наконец, ясное объяснение :) –

29

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

conceptual code implementing std::forward

Функция move в коде std::move. В этом разговоре есть (рабочая) реализация. Я нашел actual implementation of std::forward in 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 с самого начала.

2

Точка, которая не была сделана кристально чистой, состоит в том, что 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)« это не работает.

+0

Хорошая точка, поэтому T && в статическом литье также следует правилам свертывания ссылок, правильно? – barney

1

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

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

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

+0

Я не думаю, что комитет C++ чувствует, что на них лежит «правильно» языковые идиомы, и даже не определяют, что такое «правильное» использование (хотя они, безусловно, могут дать рекомендации). С этой целью, в то время как у преподавателей, боссов и друзей человека есть обязанность управлять ими так или иначе, я считаю, что комитет C++ (и, следовательно, стандарт) не имеет такой обязанности. – SirGuy

+0

Да, я только что прочитал [N2951] (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2951.html), и я согласен с тем, что стандартный комитет не обязан добавлять ненужные ограничения в отношении использования функции. Но имена этих двух шаблонов функций (перемещение и перемотка вперед) действительно немного запутывают, видя только их определения в файле библиотеки или стандартной документации (23.2.5 Помощники переадресации/перемещения). Примеры в стандарте определенно помогают понять концепцию, но было бы полезно добавить дополнительные замечания, чтобы сделать вещи более понятными. – colin