6

我真的很喜欢使用cmcstl2,这是 Ranges TS 的一个实现。我特别喜欢每个 STL 算法的可选投影。Invocable类型像这样被转发(嗯......或不):(min_element.hpp

template <ForwardIterator I, Sentinel<I> S,
    class Comp = less<>, class Proj = identity>
requires
    IndirectStrictWeakOrder<
        Comp, projected<I, Proj>>()
I min_element(I first, S last, Comp comp = Comp{}, Proj proj = Proj{});

template <ForwardRange Rng, class Comp = less<>, class Proj = identity>
requires
    IndirectStrictWeakOrder<
        Comp, projected<iterator_t<Rng>, Proj>>()
safe_iterator_t<Rng>
min_element(Rng&& rng, Comp comp = Comp{}, Proj proj = Proj{})
{
    return __stl2::min_element(__stl2::begin(rng), __stl2::end(rng),
        __stl2::ref(comp), __stl2::ref(proj));
}

作为参考:range-v3库是这样实现的:(min_element.hpp

struct min_element_fn {
        template<typename I, typename S, typename C = ordered_less, typename P = ident,
            CONCEPT_REQUIRES_(ForwardIterator<I>() && Sentinel<S, I>() &&
                IndirectRelation<C, projected<I, P>>())>
        I operator()(I begin, S end, C pred = C{}, P proj = P{}) const;

        template<typename Rng, typename C = ordered_less, typename P = ident,
            typename I = range_iterator_t<Rng>,
            CONCEPT_REQUIRES_(ForwardRange<Rng>() &&
                IndirectRelation<C, projected<I, P>>())>
        range_safe_iterator_t<Rng> operator()(Rng &&rng, C pred = C{}, P proj = P{}) const
        {
            return (*this)(begin(rng), end(rng), std::move(pred), std::move(proj));
        }
};

现在我试图理解这两种方法的区别和推理。我为什么要按Invocable值取类型?为什么我不应该对这些类型使用完美转发?

与第一种方法相比,我更了解第二种方法,因为我了解按值获取接收器参数的方法。

4

2 回答 2

8

两个原因:

  1. 我对标准库规范的阅读是,算法可以根据需要多次复制用户函数对象,但被指定为在单个实例上执行所有调用。由于 cmcstl2 经常根据其他算法实现算法,因此满足该要求的最简单方法是在内部通过reference_wrapper. 例如,binary_search调用lower_bound然后确定下界表示的元素是否完全匹配。它将reference_wrappers 传递给比较和投影函数对象,lower_bound以便以后可以调用相同的实例。

  2. 大型和/或可变函数对象可能很少见,但没有理由必须在标准库中对它们提供较差的支持。复制通常很便宜,移动几乎总是如此,但通过引用传递“从不”昂贵。cmcstl2 最小化用户函数对象的两个副本和移动。(“never”上的引号表示通过引用传递给优化器带来了相当大的负载,增加了编译时间,并且如果别名分析被函数对象引用混淆,则可能在极端情况下生成糟糕的代码。)

这个推理有一些明显的漏洞。对我来说最重要的是“如果函数对象可能有用地是有状态的,那么算法是否应该返回它们以保持该状态,就像那样std::for_each?” cmcstl2 的设计本质上违反了 Elements of Programming 所说的“有用回报法则”。我们是否应该使标准算法的签名复杂化以返回多达三个函数对象——比如一个比较器和两个投影——以适应 0.1% 的用例?我认为这里明显的答案是“不”,特别是考虑到解决方法非常简单:通过reference_wrapper.

那么,当解决方法类似地传递 a 时,为什么 cmcstl2 通常 -std::for_each尤其是标准 C++ - 会不遗余力地容纳大型和/或可变函数对象reference_wrapper?似乎 cmcstl2 的设计者在这里犯了与 LWGstd::for_each返回其函数对象时相同的错误。

于 2017-02-21T17:58:31.857 回答
7

按值取值是传统的做法Invocable,因为它们往往具有较小的sizeof,例如在函数指针或带有少量捕获的 lambda 中。根据 ABI,这些函数参数在机器寄存器中传递,或者在 or 的情况下完全less消除identity。另一方面,通过引用传递往往会促使编译器将真实对象放入 RAM。

较大的对象,或具有重要可变状态的对象,可以通过std::ref. 结果std::reference_wrapper是可简单复制的,并且与指针一样大,因此可以有效地按值传递。

于 2017-02-21T08:09:47.823 回答