Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why use a perfectly forwarded value (a functor)?

C++11 (and C++14) introduces additional language constructs and improvements that target generic programming. These include features such as;

  • R-value references
  • Reference collapsing
  • Perfect forwarding
  • Move semantics, variadic templates and more

I was browsing an earlier draft of the C++14 specification (now with updated text) and the code in an example in §20.5.1, Compile-time integer sequences, that I found interesting and peculiar.

template<class F, class Tuple, std::size_t... I> decltype(auto) apply_impl(F&& f, Tuple&& t, index_sequence<I...>) {   return std::forward<F>(f)(std::get<I>(std::forward<Tuple>(t))...); }  template<class F, class Tuple> decltype(auto) apply(F&& f, Tuple&& t) {   using Indices = make_index_sequence<std::tuple_size<Tuple>::value>;   return apply_impl(std::forward<F>(f), std::forward<Tuple>(t), Indices()); } 

Online here [intseq.general]/2.

Question

  • Why was the function f in apply_impl being forwarded, i.e. why std::forward<F>(f)(std::get...?
  • Why not just apply the function as f(std::get...?
like image 716
Niall Avatar asked Jul 16 '14 11:07

Niall


People also ask

Why do we need perfect forwarding?

Perfect forwarding reduces excessive copying and simplifies code by reducing the need to write overloads to handle lvalues and rvalues separately.

Why functors are needed?

The functor is general, and adds whatever you initialized it with), and they are also potentially more efficient. In the above example, the compiler knows exactly which function std::transform should call. It should call add_x::operator() . That means it can inline that function call.


1 Answers

In Brief...

The TL;DR, you want to preserve the value category (r-value/l-value nature) of the functor because this can affect the overload resolution, in particular the ref-qualified members.

Function definition reduction

To focus on the issue of the function being forwarded, I've reduced the sample (and made it compile with a C++11 compiler) to;

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)...); } 

And we create a second form, where we replace the std::forward(func) with just 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)...); } 

Sample evaluation

Evaluating some empirical evidence of how this behaves (with conforming compilers) is a neat starting point for evaluating why the code example was written as such. Hence, in addition we will define a general functor;

struct Functor1 {   int operator()(int id) const   {     std::cout << "Functor1 ... " << id << std::endl;     return id;   } }; 

Initial sample

Run some sample code;

int main() {   Functor1 func1;   apply_impl_2(func1, 1);   apply_impl_2(Functor1(), 2);   apply_impl(func1, 3);   apply_impl(Functor1(), 4); } 

And the output is as expected, independent of whether an r-value is used Functor1() or an l-value func when making the call to apply_impl and apply_impl_2 the overloaded call operator is called. It is called for both r-values and l-values. Under C++03, this was all you got, you could not overload member methods based on the "r-value-ness" or "l-value-ness" of the object.

Functor1 ... 1
Functor1 ... 2
Functor1 ... 3
Functor1 ... 4

Ref-qualified samples

We now need to overload that call operator to stretch this a little further...

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;   } }; 

We run another sample set;

int main() {   Functor2 func2;   apply_impl_2(func2, 5);   apply_impl_2(Functor2(), 6);   apply_impl(func2, 7);   apply_impl(Functor2(), 8); } 

And the output is;

Functor2 &... 5
Functor2 &... 6
Functor2 &... 7
Functor2 &&... 8

Discussion

In the case of apply_impl_2 (id 5 and 6), the output is not as may have been initially been expected. In both cases, the l-value qualified operator() is called (the r-value is not called at all). It may have been expected that since Functor2(), an r-value, is used to call apply_impl_2 the r-value qualified operator() would have been called. The func, as a named parameter to apply_impl_2, is an r-value reference, but since it is named, it is itself an l-value. Hence the l-value qualified operator()(int) const& is called in both the case of the l-value func2 being the argument and the r-value Functor2() being used as the argument.

In the case of apply_impl (id 7 and 8) the std::forward<F>(func) maintains or preserves the r-value/l-value nature of the argument provided for func. Hence the l-value qualified operator()(int) const& is called with the l-value func2 used as the argument and the r-value qualified operator()(int)&& when the r-value Functor2() is used as the argument. This behaviour is what would have been expected.

Conclusions

The use of std::forward, via perfect forwarding, ensures that we preserve the r-value/l-value nature of the original argument for func. It preserves their value category.

It is required, std::forward can and should be used for more than just forwarding arguments to functions, but also when the use of an argument is required where the r-value/l-value nature must be preserved. Note; there are situations where the r-value/l-value cannot or should not be preserved, in these situations std::forward should not be used (see the converse below).

There are many examples popping up that inadvertently lose the r-value/l-value nature of the arguments via a seemingly innocent use of an r-value reference.

It has always been hard to write well defined and sound generic code. With the introduction of r-value references, and reference collapsing in particular, it has become possible to write better generic code, more concisely, but we need to be ever more aware of what the original nature of the arguments provided are and make sure that they are maintained when we use them in the generic code we write.

Full sample code can be found here

Corollary and converse

  • A corollary of the question would be; given reference collapsing in a templated function, how is the r-value/l-value nature of the argument maintained? The answer - use std::forward<T>(t).
  • Converse; does std::forward solve all your "universal reference" problems? No it doesn't, there are cases where it should not be used, such as forwarding the value more than once.

Brief background to perfect forwarding

Perfect forwarding may be unfamiliar to some, so what is perfect forwarding?

In brief, perfect forwarding is there to ensure that the argument provided to a function is forwarded (passed) to another function with the same value category (basically r-value vs. l-value) as originally provided. It is typically used with template functions where reference collapsing may have taken place.

Scott Meyers gives the following pseudo code in his Going Native 2013 presentation to explain the workings of std::forward (at approximately the 20 minute mark);

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);   } } 

Perfect forwarding depends on a handful of fundamental language constructs new to C++11 that form the bases for much of what we now see in generic programming:

  • Reference collapsing
  • Rvalue references
  • Move semantics

The use of std::forward is currently intended in the formulaic std::forward<T>, understanding how std::forward works helps understand why this is such, and also aids in identifying non-idiomatic or incorrect use of rvalues, reference collapsing and ilk.

Thomas Becker provides a nice, but dense write up on the perfect forwarding problem and solution.

What are ref-qualifiers?

The ref-qualifiers (lvalue ref-qualifier & and rvalue ref-qualifier &&) are similar to the cv-qualifiers in that they (the ref-qualified members) are used during overload resolution to determine which method to call. They behave as you would expect them to; the & applies to lvalues and && to rvalues. Note: Unlike cv-qualification, *this remains an l-value expression.

like image 132
Niall Avatar answered Sep 24 '22 10:09

Niall