Які основні цілі використання std :: forward та які проблеми вона вирішує?


438

У ідеальній переадресації std::forwardвикористовується для перетворення названих посилань на rvalue 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));
}

Відповіді:


789

Ви повинні зрозуміти проблему переадресації. Ви можете прочитати всю проблему докладно , але я підсумую її.

В основному, враховуючи вираз 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 аргументів вимагає 2 N комбінацій, кошмар. Ми хотіли б зробити це автоматично.

(Це ефективно те, що ми робимо компілятор для нас у C ++ 11.)


У C ++ 11 ми отримуємо шанс виправити це. Одне рішення модифікує правила виведення шаблонів для існуючих типів, але це потенційно порушує велику кількість коду. Тож треба знайти інший шлях.

Рішення полягає в тому, щоб замість цього використовувати щойно додані rvalue-посилання ; ми можемо запровадити нові правила при виведенні типових рефератів і створити будь-який бажаний результат. Зрештою, ми зараз не можемо зламати код.

Якщо дано посилання на посилання (примітка посилання є загальним терміном, що означає і те, T&і T&&), ми використовуємо наступне правило, щоб визначити отриманий тип:

"[задано] тип TR, який є посиланням на тип T, спроба створити тип" посилання на значення cv TR "створює тип" посилання на значення 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)

Далі, з вирахуванням аргументу шаблону: якщо аргумент є значенням A, ми подаємо аргумент шаблону з посиланням на lvalue на A. В іншому випадку ми виводимо нормально. Це дає так звані універсальні посилання (термін пересилання на переадресацію зараз є офіційним).

Чому це корисно? Оскільки в поєднанні ми підтримуємо можливість відслідковувати категорію значень типу: якщо це було значення lvalue, у нас є параметр lvalue-еталон, інакше у нас є параметр rvalue-посилання.

У коді:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

Останнє - «переслати» ціннісну категорію змінної. Майте на увазі, щойно всередині функції параметр може бути переданий як значення для чого-небудь:

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

Це не добре. Потрібно отримати той самий тип вартісної категорії, який ми отримали! Рішення таке:

static_cast<T&&>(x);

Що це робить? Подумайте, що ми всередині deduceфункції, і нам було прийнято значення. Це означає, що Tє A&цільовим типом цільового типу A& &&, або просто A&. Оскільки xце вже є A&, ми нічого не робимо, і нам залишається посилання на значення.

Коли нам передано оцінку, Tє A, таким чином, цільовим типом для статичного ролі є A&&. Кінець призводить до виразу rvalue, який більше не може бути переданий до посилання на значення . Ми зберегли категорію значення параметра.

Поєднання цих даних дає нам "ідеальне переадресація":

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

Коли fотримує значення, Eотримує значення. Коли fотримує оцінку, Eотримує оцінку. Ідеально.


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

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

1
Це не fбуло б функцією, а не виразом?
Майкл Фукаракіс

1
Ваша остання спроба неправильна щодо постановки проблеми: вона передасть значення const як non-const, таким чином, взагалі не пересилає. Також зауважте, що в першій спробі const int iзаповіт буде прийнято: Aвиводиться на const int. Невдачі - для буквальних значень. Також зауважте, що для виклику до deduced(1), x є int&&, ні int(ідеальне переадресація ніколи не робить копію, як це було б зроблено, якби xбув параметр по значенню). Просто Tє int. Причина, що xоцінює значення lvalue в експедиторі, полягає в тому, що названі посилання rvalue стають виразами lvalue.
Йоханнес Шауб - ліб

5
Чи є різниця у використанні forwardчи moveтут? Або це просто смислова різниця?
0x499602D2

28
@David: std::moveслід викликати без явних аргументів шаблону і завжди призводить до переоцінки, в той час як std::forwardможе закінчитися як будь-яким. Використовуйте, std::moveколи знаєте, що вам більше не потрібне значення і хочете перемістити його в інше місце, використовуйте std::forwardце відповідно до значень, переданих вашому шаблону функції.
GManNickG

5
Дякую, що спершу розпочали конкретні приклади та мотивували проблему; дуже корисний!
ShreevatsaR

61

Я думаю, що до обговорення можна додати концептуальний код, що реалізує std :: forward. Це слайд із розмови Скотта Майєра Ефективний провідник C ++ 11/14

концептуальний код реалізації std :: вперед

Функція 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 від початку.


2
Ваше друге посилання в кінцевому підсумку вказує десь зовсім інше.
Фарап

34

У ідеальній переадресації std :: forward використовується для перетворення названих посилань rvalue t1 і t2 в неназвані посилання rvalue. Яка мета цього робити? Як би це вплинуло на функцію, що називається внутрішньою, якщо залишити t1 & t2 як значення?

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. Отже, t1in outerзавжди є виразом lvalue, тоді як forward<T1>(t1)може бути виразом rvalue залежно від T1. Останнє є лише виразом значення, якщо T1є посиланням на значення. І T1виводиться лише як посилання на значення, якщо перший аргумент до зовнішнього був виразом lvalue.


Це своєрідне поливане пояснення, але дуже добре зроблене та функціональне пояснення. Люди повинні спочатку прочитати цю відповідь, а потім заглибитись за бажанням
NicoBerrogorry

@sellibitze Ще одне питання, яке твердження є правильним при виведенні int a; f (a): "оскільки a є значенням, то int (T&&) прирівнюється до int (int & &&)" або ", щоб T && прирівнювалося до int &, значить, T має бути int & "? Я віддаю перевагу останньому.
Іван

11

Як би це вплинуло на викликану функцію внутрішню, якщо залишити t1 & t2 як значення?

Якщо після інстанцірованія, T1має тип char, і T2має клас, ви хочете передати t1за екземпляр і t2в якості constпосилання. Ну, якщо не inner()приймати їх за невідповідність const, тобто в такому випадку ви теж хочете зробити це.

Спробуйте написати набір outer()функцій, які реалізують це без рецензійних посилань, вивівши правильний спосіб передачі аргументів inner()типу 's'. Я думаю, вам знадобиться щось 2 ^ 2 з них, досить здоровенні шаблонні мета-речі для виведення аргументів, і багато часу, щоб отримати це право для всіх випадків.

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

А потім уявіть, що ви хочете зробити це за п’ятьма параметрами. Або сім.

Тепер ви знаєте, чому деякі яскраві уми придумали "ідеальне переадресація": Це змушує компілятора робити все це за вас.


5

Суть, яка не стала чітко зрозумілою, - це те, що вони також 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)", це не працює.


хороший момент, тож T&& в статичному ролях також дотримується правил, що руйнуються, правда?
barney

3

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

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

На мою думку, рух і вперед - це моделі дизайну, які є природними результатами після введення базового типу r-значення. Ми не повинні називати метод, припускаючи, що він правильно використовується, якщо неправильне використання заборонено.


Я не думаю, що комітет C ++ вважає, що зловживання їм належить використовувати мовні ідіоми "правильно", а також не визначати, що таке "правильне" використання (хоча вони, безумовно, можуть дати вказівки). З цією метою, хоча викладачі, начальники та друзі людини можуть мати обов'язок керувати ними так чи інакше, я вважаю, що комітет C ++ (і, отже, стандарт) не має цього обов'язку.
SirGuy

Так, я просто прочитав N2951 і погоджуюся, що стандартний комітет не зобов'язаний додавати зайві обмеження щодо використання функції. Але імена цих двох шаблонів функцій (переміщення і пересування вперед) дійсно трохи заплутані, бачачи лише їхні визначення у файлі бібліотеки або стандартній документації (23.2.5 Помічники вперед / переміщення). Приклади стандарту, безумовно, допомагають зрозуміти концепцію, але може бути корисним додати більше зауважень, щоб зробити речі яснішими.
colin
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.