Как работает std:: forward?

Возможный дубликат:
Преимущества использования переадресации

Я знаю, что он делает и когда его использовать, но я все еще не могу окунуться в то, как он работает. Пожалуйста, как можно подробнее и объясните, когда std::forward будет некорректным, если разрешено использовать вывод аргумента шаблона.

Часть моей путаницы такова: "Если у него есть имя, это значение lvalue" - если в случае, если std::forward ведет себя по-другому, когда я передаю thing&& x vs thing& x?

Ответ 1

Во-первых, давайте посмотрим, что 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.

Ответ 2

Я думаю, что объяснение std::forward как static_cast<T&&> сбивает с толку. Наша интуиция для приведения заключается в том, что он преобразует тип в какой-то другой тип - в этом случае это будет преобразование в ссылку на rvalue. Это не так! Итак, мы объясняем одну загадочную вещь, используя другую загадочную вещь. Этот конкретный состав определен таблицей в ответе Xeo. Но вопрос: почему? Итак, мое понимание:

Предположим, я хочу передать вам std::vector<T> v, который вы должны хранить в своей структуре данных как элемент данных _v. Наивным (и безопасным) решением было бы всегда копировать вектор в конечный пункт назначения. Поэтому, если вы делаете это через посредническую функцию (метод), эта функция должна быть объявлена как получающая ссылку. (Если вы объявите, что он принимает вектор по значению, вы будете выполнять дополнительную полностью ненужную копию.)

void set(const 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(const 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. Таким образом, магия С++ 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, компилятор сохранит его значение и будет вызвана эталонная версия set(). Эта версия перемещает содержимое t, что означает, что оригинал становится пустым.

Этот ответ оказался намного длиннее, чем я изначально предполагал ;-)

Ответ 3

Это работает, потому что, когда вызывается безусловная переадресация, тип 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 обнаруживает через вышеописанные средства, что на самом деле это rvalue, и правильно возвращает rvalue для пересылки.