Как работает std::forward?
Возможный дубликат:
Преимущества использования форварда
Я знаю, что он делает и когда его использовать, но все еще не могу понять, как он работает. Пожалуйста, будьте максимально подробны и объясните, когда std::forward
было бы неправильно, если бы было разрешено использовать вывод аргумента шаблона.
Часть моего замешательства заключается в следующем:
"Если у него есть имя, это lvalue" - если это так, почему std::forward
вести себя по-другому, когда я прохожу thing&& x
против thing& x
?
3 ответа
Во-первых, давайте посмотрим на то, что std::forward
делает в соответствии со стандартом:
§20.2.3 [forward] p2
Возвращает:
static_cast<T&&>(t)
(Куда T
это явно указанный параметр шаблона и t
это переданный аргумент.)
Теперь запомните правила свертывания ссылок:
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)
(Бесстыдно украден из этого ответа.)
А затем давайте посмотрим на класс, который хочет использовать идеальную пересылку:
template<class T>
struct some_struct{
T _v;
template<class U>
some_struct(U&& v)
: _v(static_cast<U&&>(v)) {} // perfect forwarding here
// std::forward is just syntactic sugar for this
};
А теперь пример вызова:
int main(){
some_struct<int> s1(5);
// in ctor: '5' is rvalue (int&&), so 'U' is deduced as 'int', giving 'int&&'
// ctor after deduction: 'some_struct(int&& v)' ('U' == 'int')
// with rvalue reference 'v' bound to rvalue '5'
// now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int&&>(v)'
// this just turns 'v' back into an rvalue
// (named rvalue references, 'v' in this case, are lvalues)
// huzzah, we forwarded an rvalue to the constructor of '_v'!
// attention, real magic happens here
int i = 5;
some_struct<int> s2(i);
// in ctor: 'i' is an lvalue ('int&'), so 'U' is deduced as 'int&', giving 'int& &&'
// applying the reference collapsing rules yields 'int&' (& + && -> &)
// ctor after deduction and collapsing: 'some_struct(int& v)' ('U' == 'int&')
// with lvalue reference 'v' bound to lvalue 'i'
// now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int& &&>(v)'
// after collapsing rules: 'static_cast<int&>(v)'
// this is a no-op, 'v' is already 'int&'
// huzzah, we forwarded an lvalue to the constructor of '_v'!
}
Я надеюсь, что этот пошаговый ответ поможет вам и другим понять, как std::forward
работает.
Я думаю, что объяснение std::forward
как static_cast<T&&>
сбивает с толку. Наша интуиция для приведения заключается в том, что он преобразует тип в какой-то другой тип - в этом случае это будет преобразование в ссылку на rvalue. Это не! Итак, мы объясняем одну загадочную вещь, используя другую загадочную вещь. Этот конкретный состав определен таблицей в ответе Xeo. Но вопрос: почему? Итак, вот мое понимание:
Предположим, я хочу передать вам std::vector<T> v
что вы должны хранить в вашей структуре данных в качестве члена данных _v
, Наивным (и безопасным) решением было бы всегда копировать вектор в конечный пункт назначения. Так что, если вы делаете это через посредническую функцию (метод), эта функция должна быть объявлена как получающая ссылку. (Если вы объявите, что он принимает вектор по значению, вы будете выполнять дополнительную ненужную копию.)
void set(std::vector<T> & v) { _v = v; }
Это нормально, если у вас в руке lvalue, но как насчет rvalue? Предположим, что вектор является результатом вызова функции makeAndFillVector()
, Если вы выполнили прямое задание:
_v = makeAndFillVector();
компилятор будет перемещать вектор, а не копировать его. Но если вы введете посредника, set()
информация о природе вашего аргумента будет потеряна, а копия будет сделана.
set(makeAndFillVector()); // set will still make a copy
Чтобы избежать этой копии, вам нужна "идеальная пересылка", которая каждый раз приводит к оптимальному коду. Если вам дано lvalue, вы хотите, чтобы ваша функция воспринимала его как lvalue и делала копию. Если вам дано значение r, вы хотите, чтобы ваша функция воспринимала его как значение и перемещала его.
Обычно вы делаете это, перегружая функцию set()
отдельно для lvalues и rvalues:
set(std::vector<T> & lv) { _v = v; }
set(std::vector<T> && rv) { _v = std::move(rv); }
Но теперь представьте, что вы пишете шаблонную функцию, которая принимает T
и звонки set()
с этим T
(не беспокойтесь о том, что наши set()
определяется только для векторов). Хитрость в том, что вы хотите, чтобы этот шаблон вызывал первую версию set()
когда функция шаблона создается с помощью lvalue, а вторая - когда она инициализируется с помощью rvalue.
Прежде всего, какой должна быть подпись этой функции? Ответ таков:
template<class T>
void perfectSet(T && t);
В зависимости от того, как вы вызываете эту функцию шаблона, тип T
будет несколько волшебным образом выведен по-другому. Если вы называете это с lvalue:
std::vector<T> v;
perfectSet(v);
вектор v
будет передано по ссылке. Но если вы называете это с помощью rvalue:
perfectSet(makeAndFillVector());
(анонимный) вектор будет передан по ссылке rvalue. Таким образом, магия C++11 преднамеренно настроена таким образом, чтобы по возможности сохранить природу аргументов rvalue.
Теперь внутри perfectSet вы хотите передать аргумент правильной перегрузке set()
, Это где std::forward
является необходимым:
template<class T>
void perfectSet(T && t) {
set(std::forward<T>(t));
}
Без std::forward компилятор должен был бы предположить, что мы хотим передать t по ссылке. Чтобы убедиться, что это правда, сравните этот код:
void perfectSet(T && t) {
set(t);
set(t); // t still unchanged
}
к этому:
void perfectSet(T && t) {
set(std::forward<T>(t));
set(t); // t is now empty
}
Если вы не явно переслать t
компилятор должен защищаться, предполагая, что вы снова обращаетесь к t, и выбрал эталонную версию lvalue для set. Но если вы перешли t
компилятор сохранит его rvalue-ness и эталонную версию rvalue set()
будет называться. Эта версия перемещает содержимое t
, что означает, что оригинал становится пустым.
Этот ответ оказался намного длиннее, чем я изначально предполагал;-)
Это работает, потому что когда вызывается совершенная пересылка, тип T не является типом значения, он также может быть ссылочным типом.
Например:
template<typename T> void f(T&&);
int main() {
std::string s;
f(s); // T is std::string&
const std::string s2;
f(s2); // T is a const std::string&
}
В качестве таких, forward
можно просто посмотреть на явный тип T, чтобы увидеть, что вы действительно передали. Конечно, точная реализация этого процесса, если я помню, нетривиальна, но здесь есть информация.
Когда вы ссылаетесь на именованную ссылку rvalue, тогда это действительно lvalue. Тем не мение, forward
обнаруживает с помощью вышеуказанного средства, что это на самом деле значение r, и правильно возвращает значение r для пересылки.