Ответ 1
Вкратце...
TL; DR, вы хотите сохранить категория значений (значение r-value/l-value ) функтора, потому что это может повлиять на разрешение перегрузки, в частности ref-qualified members.
Сокращение определения функции
Чтобы сосредоточиться на проблеме пересылаемой функции, я уменьшил образец (и скомпилировал его с компилятором С++ 11):
template<class F, class... Args>
auto apply_impl(F&& func, Args&&... args) -> decltype(std::forward<F>(func)(std::forward<Args>(args)...)) {
return std::forward<F>(func)(std::forward<Args>(args)...);
}
И мы создаем вторую форму, где мы заменяем std::forward(func)
только func
;
template<class F, class... Args>
auto apply_impl_2(F&& func, Args&&... args) -> decltype(func(std::forward<Args>(args)...)) {
return func(std::forward<Args>(args)...);
}
Пример оценки
Оценка некоторых эмпирических доказательств того, как это ведет (с соответствующими компиляторами), является аккуратной отправной точкой для оценки того, почему пример кода был написан как таковой. Следовательно, кроме того, мы определим общий функтор:
struct Functor1 {
int operator()(int id) const
{
std::cout << "Functor1 ... " << id << std::endl;
return id;
}
};
Исходный образец
Запустите некоторый пример кода;
int main()
{
Functor1 func1;
apply_impl_2(func1, 1);
apply_impl_2(Functor1(), 2);
apply_impl(func1, 3);
apply_impl(Functor1(), 4);
}
И вывод будет таким, как ожидалось, независимо от того, используется ли r-значение Functor1()
или l-value func
при вызове apply_impl
и apply_impl_2
вызывается оператор перегруженного вызова. Он вызывается как для r-значений, так и для l-значений. В С++ 03 это все, что у вас есть, вы не можете перегружать методы-члены, основанные на "r-value-ness" или "l-value-ness" объекта.
Functor1... 1
Functor1... 2
Functor1... 3
Functor1... 4
Проверенные образцы
Теперь нам нужно перегрузить этот оператор вызова, чтобы растянуть его немного дальше...
struct Functor2 {
int operator()(int id) const &
{
std::cout << "Functor2 &... " << id << std::endl;
return id;
}
int operator()(int id) &&
{
std::cout << "Functor2 &&... " << id << std::endl;
return id;
}
};
Мы запускаем еще один набор образцов;
int main()
{
Functor2 func2;
apply_impl_2(func2, 5);
apply_impl_2(Functor2(), 6);
apply_impl(func2, 7);
apply_impl(Functor2(), 8);
}
И результат:
Functor2 &... 5
Functor2 &... 6
Functor2 &... 7
Functor2 &... 8
Обсуждение
В случае apply_impl_2
(id
5 и 6) выход не такой, как это было первоначально ожидалось. В обоих случаях определяется l-значение operator()
(значение r вообще не вызывается). Можно было ожидать, что, поскольку Functor2()
, r-значение используется для вызова apply_impl_2
, было бы вызвано r-значение, квалифицированное operator()
. func
, как именованный параметр apply_impl_2
, является ссылкой на r-значение, но поскольку он назван, он сам является l-значением. Следовательно, l-значение, квалифицированное operator()(int) const&
, вызывается как в случае l-value func2
, так и в качестве аргумента используется r-значение Functor2()
.
В случае apply_impl
(id
7 и 8) std::forward<F>(func)
поддерживает или сохраняет значение r-value/l-value аргумента, предоставленного для func
. Следовательно, l-value, квалифицированный operator()(int) const&
вызывается с использованием l-value func2
, используемого в качестве аргумента, и r-значение, квалифицированное operator()(int)&&
, когда в качестве аргумента используется значение r Functor2()
. Такое поведение было бы ожидаемым.
Выводы
Использование std::forward
посредством совершенной пересылки гарантирует, что мы сохраним значение r-value/l-value исходного аргумента для func
. Он сохраняет их категория значений.
Требуется, std::forward
может и должно использоваться не только для пересылки аргументов в функции, но также и при использовании аргумента, где необходимо сохранить значение r-value/l-value. Заметка; существуют ситуации, когда значение r-value/l не может или не должно сохраняться, в таких ситуациях std::forward
не следует использовать (см. обратное ниже).
Появляется много примеров, которые непреднамеренно теряют характер аргументов r-value/l-value через кажущееся невиновным использование ссылки r-value.
Всегда было сложно написать четко определенный и звуковой общий код. С введением ссылок на r-значение и, в частности, сбрасыванием ссылок стало возможным писать более качественный общий код, более кратко, но нам нужно быть более осведомленным о том, каков первоначальный характер предоставленных аргументов и убедиться, что они поддерживаются, когда мы используем их в общем коде, который мы пишем.
Полный образец кода можно найти здесь
Следствие и обратное
- Следствием этого вопроса будет; заданная ссылка, свернутая в шаблонизированной функции, как сохраняется аргумент аргумента r-value/l-value? Ответ - используйте
std::forward<T>(t)
. - Converse;
std::forward
решает все ваши проблемы с "универсальной ссылкой"? Нет, нет, бывают случаи, когда не следует использовать, например, пересылать значение больше чем один раз.
Краткое описание идеальной пересылки
Отличная переадресация может быть незнакомой некоторым, так что отличная пересылка?
Вкратце, идеальная пересылка должна гарантировать, что аргумент, предоставляемый функции, пересылается (передается) другой функции с той же категорией значений (в основном, r-value vs. l-value) как изначально предоставлено. Обычно он используется с функциями шаблона, где может произойти сбрасывание ссылок.
Скотт Мейерс дает следующий псевдокод в своем Going Native 2013 presentation, чтобы объяснить работуstd::forward
(примерно с отметкой 20 минут);
template <typename T>
T&& forward(T&& param) { // T&& here is formulated to disallow type deduction
if (is_lvalue_reference<T>::value) {
return param; // return type T&& collapses to T& in this case
}
else {
return move(param);
}
}
Идеальная пересылка зависит от нескольких фундаментальных конструктов языка, новых для С++ 11, которые составляют основу большей части того, что мы теперь видим в общем программировании:
- Сбой ссылок
- Ссылки на Rvalue
- Переместить семантику
Использование std::forward
в настоящее время предназначено в формуле std::forward<T>
, понимание того, как работает std::forward
, помогает понять, почему это так, а также помогает идентифицировать неидиоматическое или неправильное использование rvalues, сведение коллапса ссылок и т.п..
Томас Беккер предлагает приятную, но плотную запись о совершенной пересылке проблема и .
Что такое ref-qualifiers?
ref-qualifiers (ref-qualifier refvalifier &
и rvalue ref-qualifier &&
) похожи на cv-квалификаторы, поскольку они (ref-qual members) используются во время разрешение перегрузки, чтобы определить, какой метод звонить. Они ведут себя так, как вы ожидали от них; &
применяется к lvalues и &&
к rvalues. Примечание: В отличие от cv-квалификации, *this
остается выражением l-value.