【问题标题】:How to properly forward Invocable types如何正确转发 Invocable 类型
【发布时间】:2017-02-21 07:52:34
【问题描述】:

我真的很喜欢使用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 类型? 为什么我不应该对这些类型使用完美转发?

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

【问题讨论】:

    标签: c++ c++17 perfect-forwarding c++-concepts range-v3


    【解决方案1】:

    两个原因:

    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

    那么,当解决方法同样是传递reference_wrapper 时,为什么通常 cmcstl2(尤其是标准 C++ 的 std::for_each)要竭尽全力容纳大型和/或可变函数对象?看来 cmcstl2 的设计者在这里犯了和 LWG 一样的错误,他们让 std::for_each 返回其函数对象。

    【讨论】:

    • 来源:我是 cmcstl2 的作者。
    • reference_wrapper 在传递一个大的/可变的临时变量时并不容易拼写。我也不认为 1) 与其他实施者的阅读相匹配,尽管它很高兴。
    • 我没想到会得到另一个答案,但我很高兴。特别是。二分搜索的例子真的很好。
    【解决方案2】:

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

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

    【讨论】:

    • 那么对于这些​​类型,是否使用std::ref 还是std::move 只是一个口味问题?复制有状态的函数对象背后有什么原因吗?改变这种状态时是否会被认为是“副作用”,而 STL 有功能根源?
    • 1.对于命名对象,是的。但通常参数是一个表达式或{}。 2.取决于你的意思是有状态的。带有[&amp;] 捕获的 lambda 是有状态但不可变的。许多捕获可能会通过按值传递降低性能(或者可能不会,取决于优化)。带有mutable 的 lambda 如果在不期望的情况下被复制,可能会出现异常。 3. 函数式哲学可能帮助 STL 忽略了自变异函子,但它们也并不常见。引用语义在 C++ 中可能常见;库默认为值有点不寻常,但它仍然是很好的设计。
    • 我是 range-v3 的作者,我同意这个消息。 :-)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-05
    • 2022-01-05
    • 1970-01-01
    相关资源
    最近更新 更多