【问题标题】:Why does `std::ranges::clamp` limits the number of projections so strictly?为什么`std::ranges::clamp`如此严格地限制投影的数量?
【发布时间】:2021-05-01 15:43:08
【问题描述】:

根据[alg.clamp#5]std::ranges::clamp 的时间复杂度最多需要 2 次比较和 3 应用预测。 cppreference 中的可能实现方式如下:

struct clamp_fn {
  template<class T, class Proj = std::identity,
           std::indirect_strict_weak_order<std::projected<const T*, Proj>> Comp = ranges::less>
  constexpr const T& operator()(const T& v, const T& lo, const T& hi,
                                Comp comp = {}, Proj proj = {}) const
  {
      assert(!std::invoke(comp, std::invoke(proj, hi), std::invoke(proj, lo)));
      return  std::invoke(comp, std::invoke(proj,  v), std::invoke(proj, lo)) ? lo
            : std::invoke(comp, std::invoke(proj, hi), std::invoke(proj,  v)) ? hi : v;
  }
};
 
inline constexpr clamp_fn clamp;

这显然不符合要求,因为它涉及3次比较和6预测。即使我们注释掉assert,投影的数量仍然是4,因为std::invoke(proj, v)被执行了两次。

我能想到的唯一方法是暂时存储std::invoke(proj, v) 的结果,然后将其传递给接下来的两个comp 调用,就像libstdc++ 所做的那样:

auto&& __proj_val = std::__invoke(__proj, __val);
if (std::__invoke(__comp, __proj_val, std::__invoke(__proj, __lo)))
  return __lo;
else if (std::__invoke(__comp, std::__invoke(__proj, __hi), __proj_val))
  return __hi;
else
  return __val;

但是为了安全,我们似乎无法在第一个comp调用中使用std::forward&lt;decltype(__proj_val)&gt;(__proj_val)来完善转发__proj_val,也就是说我们似乎无法使用只有3个完美的预测实现了std::ranges::clamp

为什么std::ranges::clamp 如此严格地限制投影数量?这是否意味着需要临时存储投影结果以满足复杂性要求?还是我对这种复杂性要求的理解有误?

【问题讨论】:

    标签: c++ time-complexity c++20 std-ranges


    【解决方案1】:

    这是非常有意的。我在布拉格的 LWG 审查论文期间特别询问了这个复杂性要求,因为它禁止“明显”的实施。是的,这需要实现调用该值的投影并使用auto&amp;&amp; 或等效项“将结果悬空”。

    它还需要完美地转发预计值(libstdc++ 无法做到这一点)。这是有效的,因为invoke 表达式需要不修改其参数(来自regular_invocable 的要求),并且因为indirect_strict_weak_order 中的任何内容都不需要iter_reference_t&lt;I1&gt;&amp; 的可调用性,只有iter_reference_t&lt;I1&gt; 和@987654327 @。

    【讨论】:

    • 等等,标准要求你(假设项目返回一个值)初始化一个值一次,然后在这里移动两次?还是我误会了?
    • @Yakk-AdamNevraumont 我认为如果返回类型是 cv-unqualified,你可以不移动它(因为在这种情况下 projected 它与值类型相同,我们确实需要iter_value_t&amp; 工作)。但是,如果投影产生const T,那么比较只需要支持T 左值和const T 右值,而无需移动,您将使用const T 左值调用。在任何情况下,比较都需要不修改它的参数,即使所述参数是右值,所以移动它两次是安全的。
    • 如果比较按值进行,则在移动构造函数中导致修改的是调用代码,而不是比较。
    • invoke表达式,要求不修改。由于invoke 始终采用引用,因此任何修改都发生在该表达式内。
    • 有没有一个投影和谓词的例子证明如果不将std::forward应用于存储的临时,谓词确实是不可调用的?我尝试了投影的 cv-ref 返回值的所有组合,但是所有它们都可以调用正确的谓词(当它们满足要求时)。你能帮我找到这样的例子吗?
    【解决方案2】:

    转发是有条件的移动。移动的意思是“我不再需要这个值了”。仅重新计算一个值以便我们可以更频繁地移动它是愚蠢的;我们的选择是创建 2 并移动两者,或者创建 1,使用它,然后在完成后移动它。

    【讨论】:

      猜你喜欢
      • 2021-12-06
      • 2020-01-11
      • 1970-01-01
      • 1970-01-01
      • 2021-05-11
      • 2018-08-29
      • 1970-01-01
      • 2022-12-15
      相关资源
      最近更新 更多