Как работает 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 для пересылки.

Другие вопросы по тегам