Вкратце...
В TL;DR вы хотите сохранить категорию значений (природа r-value/l-value) функтора, потому что это может повлиять на разрешение перегрузки, в частности квалифицированный по ссылке участники.
Сокращение определения функции
Чтобы сосредоточиться на проблеме переадресации функции, я сократил образец (и компилировал его с помощью компилятора C++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-значение func
при вызове apply_impl
и apply_impl_2
вызывается оператор перегруженного вызова. Он вызывается как для r-значений, так и для l-значений. В C++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);
}
И выход;
Функтор2 &... 5
Функтор2 &... 6
Функтор2 &... 7
Функтор2 &&... 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-значение func2
является аргументом, так и в случае использования r-значения Functor2()
в качестве аргумента.
В случае apply_impl
(id
7 и 8) std::forward<F>(func)
поддерживает или сохраняет природу r-значения/l-значения аргумента, предусмотренного для func
. Следовательно, l-значение с указанием operator()(int) const&
вызывается с l-значением func2
, используемым в качестве аргумента, и r-значением с уточнением operator()(int)&&
, когда в качестве аргумента используется r-значение Functor2()
. Такое поведение было ожидаемым.
Выводы
Использование std::forward
посредством идеальной переадресации гарантирует, что мы сохраним характер r-value/l-value исходного аргумента для func
. Он сохраняет их категорию значений.
Это необходимо, std::forward
можно и нужно использовать не только для переадресации аргументов функциям, но также и тогда, когда требуется использование аргумента, где природа r-value/l-value должна быть сохраненным. Примечание; бывают ситуации, когда r-значение/l-значение не может или не должно сохраняться, в этих ситуациях std::forward
не следует использовать (см. обратное ниже).
Появляется много примеров, в которых непреднамеренно теряется характер аргументов r-значение/l-значение из-за, казалось бы, невинного использования ссылки на r-значение.
Всегда было трудно писать хорошо определенный и понятный универсальный код. С введением ссылок на r-значения и, в частности, свертывания ссылок стало возможным писать лучший общий код, более лаконичный, но нам нужно как можно лучше понимать, какова исходная природа предоставленных аргументов, и убедиться, что они сохраняются, когда мы используем их в общем коде, который мы пишем.
Полный пример кода можно найти здесь
Следствие и обратное
- Следствием вопроса будет; если ссылка рушится в шаблонной функции, как поддерживается характер аргумента r-value/l-value? Ответ - используйте
std::forward<T>(t)
.
- Конверс; решает ли
std::forward
все ваши проблемы с универсальными ссылками? Нет, бывают случаи, когда не следует использовать, например, пересылка значения более одного раза.
Краткая предыстория идеальной переадресации
Идеальная переадресация может быть незнакома некоторым, так что же такое идеальная переадресация?
Короче говоря, идеальная переадресация предназначена для того, чтобы гарантировать, что аргумент, предоставленный функции, будет перенаправлен (передан) другой функции с той же категорией значений (в основном r-значение против l-значения), что и предоставлено изначально. Обычно он используется с функциями шаблона, где свертывание ссылок могло иметь место.
Скотт Мейерс приводит следующий псевдокод в своем Презентация Going Native 2013 для объяснения работы 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);
}
}
Идеальная переадресация зависит от нескольких фундаментальных языковых конструкций, новых для C++11, которые формируют основу для большей части того, что мы сейчас видим в обобщенном программировании:
- Ссылка сворачивается
- Ссылки Rvalue
- Семантика перемещения
Использование std::forward
в настоящее время предназначено в формульном std::forward<T>
, понимание того, как работает std::forward
, помогает понять, почему это так, а также помогает определить неидиоматическое или неправильное использование rvalue, свертывание ссылок и тому подобное.
Томас Беккер (Thomas Becker) дает хороший, но емкий отчет о проблеме и решение.
Что такое реф-квалификаторы?
ref-qualifiers (lvalue ref-qualifier &
и rvalue ref-qualifier &&
) аналогичны cv-qualifiers тем, что они (члены с квалификацией ref) используются во время разрешение перегрузки, чтобы определить, какой метод вызывать. Они ведут себя так, как вы от них ожидаете; &
применяется к lvalue и &&
к rvalue. Примечание. В отличие от квалификации cv, *this
остается выражением l-значения.
person
Niall
schedule
16.07.2014