Search code examples
c++lambdac++14move-semanticsperfect-forwarding

Meaning of std::forward<std::decay_t<F>>(f)


I have a question about the code written in https://koturn.hatenablog.com/entry/2018/06/10/060000 When I pass a left value reference, if I do not remove the reference with std::decay_t, I get an error.
Here is the error message
'error: 'operator()' is not a member of 'main()::<lambda(auto:11, int)>&
I don't understand why it is necessary to exclude the left value reference.
I would like to know what this error means.

#include <iostream>
#include <utility>


template <typename F>
class
FixPoint : private F
{
public:
  explicit constexpr FixPoint(F&& f) noexcept
    : F{std::forward<F>(f)}
  {}

  template <typename... Args>
  constexpr decltype(auto)
  operator()(Args&&... args) const
  {
    return F::operator()(*this, std::forward<Args>(args)...);
  }
};  // class FixPoint


namespace
{
template <typename F>
inline constexpr decltype(auto)
makeFixPoint(F&& f) noexcept
{
  return FixPoint<std::decay_t<F>>{std::forward<std::decay_t<F>>(f)};
}
}  // namespace


int
main()
{
  auto body = [](auto f, int n) -> int {
    return n < 2 ? n : (f(n - 1) + f(n - 2));
  };
  auto result = makeFixPoint(body)(10);
  std::cout << result << std::endl;
}


Solution

  • ,The code you have shared is toxic.

    template <typename F>
    class FixPoint : private F
    {
    public:
      explicit constexpr FixPoint(F&& f)
    

    what this means is that we expect F to be a value type (because inheriting from a reference isn't possible). In addition, we will only accept rvalues -- we will only move from another F.

      : F{std::forward<F>(f)}
    {}
    

    this std::forward<F> is pointless; this indicates that the person writing this code thinks they are perfect forwarding: they are not. The only legal types F are value types (not references), and if F is a value type F&& is always an rvalue reference, and thus std::forward<F> is always equivalent to std::move.

    There are cases where you want to

    template<class X>
    struct wrap1 {
      X&& x;
      wrap1(X&& xin):x(std::forward<X>(xin)){}
    };
    

    or even

    template<class X>
    struct wrap2 {
      X x;
      wrap2(X&& xin):x(std::forward<X>(xin)){}
    };
    

    so the above code is similar to some use cases, but it isn't one of those use cases. (The difference here is that X or X&& is the type of a member, not a base class; base classes cannot be references).

    The use for wrap2 is when you want to "lifetime extend" rvalues, and simply take references to lvalues. The use for wrap1 is when you want to continue the perfect forwarding of some expression (wrap1 style objects are generally unsafe to keep around for more than a single line of code; wrap2 are safe so long as they don't outlive any lvalue passed to them).


    template <typename F>
    inline constexpr decltype(auto)
    makeFixPoint(F&& f) noexcept
    {
      return FixPoint<std::decay_t<F>>{std::forward<std::decay_t<F>>(f)};
    }
    

    Ok more red flags here. inline constexpr is a sign of nonsense; constexpr functions are always inline. There could be some compilers who treat the extra inline as meaning something, so not a guarantee of a problem.

    return FixPoint<std::decay_t<F>>{std::forward<std::decay_t<F>>(f)};
    

    std::forward is a conditional move. It moves if there is an rvalue or value type passed to it. decay is guaranteed to return a value type. So we just threw out the conditional part of the operation, and made it into a raw move.

    return FixPoint<std::decay_t<F>>{std::move(f)};
    

    this is a simpler one that has the same meaning.

    At this point you'll probably see the danger:

    template <typename F>
    constexpr decltype(auto)
    makeFixPoint(F&& f) noexcept
    {
      return FixPoint<std::decay_t<F>>{std::move(f)};
    }
    

    we are unconditionally moving from the makeFixPoint argument. There is nothing about the name makeFixPoint that says "I move from the argument", and it accept forwarding references; so it will silently consume an lvalue and move-from it.

    This is very rude.


    So a sensible version of the code is:

    template <typename F>
    class FixPoint : private F
    {
    public:
      template<class U,
        class dU = std::decay_t<U>,
        std::enable_if_t<!std::is_same_v<dU, FixPoint> && std::is_same_v<dU, F>, bool> = true
      >
      explicit constexpr FixPoint(U&& u) noexcept
        : F{std::forward<U>(u)}
      {}
    

    [SNIP]

    namespace
    {
      template <typename F>
      constexpr FixPoint<std::decay_t<F>>
      makeFixPoint(F&& f) noexcept
      {
        return FixPoint<std::decay_t<F>>{std::forward<F>(f)};
      }
    }  // namespace
    

    that cleans things up a bit.