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