std::forward<std::decay_t<F>>(f) 的含义

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

我对https://koturn.hatenablog.com/entry/2018/06/10/060000中编写的代码有疑问 当我传递一个左值引用时,如果我不使用 std::decay_t 删除引用,我会得到一个错误。
这是错误信息
'错误:'operator()' 不是'main()::&
的成员 不明白为什么要排除左值引用
我想知道这个错误是什么意思。

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

模板无法按预期方式工作。

首先,因为应该可以继承自F,所以使用std::decay_t意义不大。相反,它可能应该只是 std::remove_cvref_t,但在 C++20 之前写起来有点麻烦,而 std::decay_t 几乎是一样的。

在 class 模板中 F 旨在成为 non-reference 类型。它不是构造函数的模板参数。所以它不能用于perfect-forwarding。构造函数参数将始终是不能采用左值的右值引用。

即使将左值传递给 makeFixPoint,调用 std::forward<std::decay_t<F>>(f) 也会强制转换为右值引用,然后可以在构造函数中使用。这显然是危险的。函数 always 的行为就像您将参数 std::moved 放入其中一样。

它应该只是std::forward<F>(f)并且构造函数应该采用另一个可用于形成转发引用的模板参数:

template<typename G>
requires std::is_same_v<std::remove_cvref_t<G>, F>
explicit constexpr FixPoint(G&& g) noexcept
  : F{std::forward<G>(g)}
{}

I don't understand why it is necessary to exclude the left value reference. I would like to know what this error means.

当您将 lvalue lambda 传递给 makeFixPoint() 时,makeFixPoint() 的模板参数 F 被实例化为 L& ,其中 L 是 lambda 类型。在函数体中,FixPoint<std::decay_t<F>>{...}会被实例化为FixPoint<L>{...},所以FixPoint的构造函数被实例化为

explicit constexpr FixPoint(L&& f);

它接受 lambda 类型的 rvalue 引用。在 makeFixPoint() 中,如果您使用 {std::forward<F>(f)} 初始化它,即 {std::forward<L&>(f)}f 将作为 lvalue 转发,这将是 ill-formed 因为 rvalue 引用不能绑定 lvalue.

使用{std::forward<std::decay_t<F>>(f)}的目的是强制f作为右值转发。

,你分享的代码有毒

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

这意味着我们期望 F 是一个值类型(因为从引用继承是不可能的)。此外,我们将只接受右值——我们将 从另一个 F.

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

这个std::forward<F>毫无意义;这表明编写此代码的人 认为 他们是完美转发:他们不是。唯一合法的类型 F 是值类型(不是引用),如果 F 是值类型 F&& 总是右值引用,因此 std::forward<F> 总是等价于std::move.

有些情况下你想要

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

甚至

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

所以上面的代码与某些用例相似,但它不是那些用例之一。 (这里的区别是XX&&是成员的类型,不是基class;基class不能是引用)。

wrap2 的用途是当你想“延长生命周期”右值,并简单地引用左值时。 wrap1 的用途是当你想继续完美转发某些表达式时(wrap1 样式对象通常不安全地保留超过一行代码;wrap2 是安全的只要它们的寿命不超过传递给它们的任何左值。


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

好的,这里有更多危险信号。 inline constexpr是废话的标志; constexpr 函数总是 inline。可能有些编译器将额外的 inline 视为某种意义,因此不能保证一定会出现问题。

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

std::forward 是有条件的着法。如果有右值或值类型传递给它,它就会移动。 decay 保证 return 一个值类型。所以我们只是去掉了操作的条件部分,把它变成了一个原始的移动。

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

这个比较简单,意思一样。

此时你可能会看到危险:

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

我们无条件地从 makeFixPoint 论点转移。名称 makeFixPoint 没有任何内容表示“我从参数中移出”,并且它接受转发引用;所以它会默默地消耗一个左值和move-from它。

这很粗鲁。


所以代码的合理版本是:

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

[截图]

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

稍微清理一下。