如何正确转发Invocable类型

Mai*_*kel 6 c++ perfect-forwarding c++-concepts range-v3 c++17

我真的很喜欢使用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));
}
Run Code Online (Sandbox Code Playgroud)

作为参考: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));
        }
};
Run Code Online (Sandbox Code Playgroud)

现在我试着理解两种方法的区别和推理.为什么我应该Invocable按价值计算类型?为什么我不应该为这些类型使用完美转发?

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

Cas*_*sey 8

两个原因:

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

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

这种推理有一些明显的漏洞.对我来说最重要的是"如果函数对象可能是有用的,那么算法是否应该返回它们以保留该状态,就像这样std::for_each?" cmcstl2的设计基本上违反了编程元素所谓的"有用回归定律".我们是否应该使标准算法的签名复杂化以返回多达三个函数对象 - 比如比较器和两个投影 - 以容纳0.1%的用例?我认为这里显而易见的答案是"不",特别是考虑到解决方法很简单:传递一个reference_wrapper.

那么,为什么cmcstl2一般 - std::for_each特别是标准C++ - 当解决方法类似于传递一个reference_wrapper?时,它们会不会适应大型和/或可变函数对象?似乎cmcstl2的设计者在LWG std::for_each返回其功能对象时犯了同样的错误.


Pot*_*ter 7

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

可以传递较大的对象或具有显着可变状态的对象std::ref.结果std::reference_wrapper是可以轻易复制的,并且与指针一样大,因此它可以通过值有效地传递.

  • 1.对于命名对象,是的.但通常参数是表达式或`{}`.2.取决于有状态你的意思.带有`[&]`捕获的lambda是有状态但不可变的.许多捕获可能会以值传递降低性能(或者可能不会,取决于优化).具有`mutable`的lambda如果在不期望的情况下被复制可能会出错.功能哲学可能帮助STL忽略了自变异函子,但它们也不常见.在C++中,引用语义可能也是常见的.库默认为值有点不寻常,但它仍然是很好的设计. (2认同)
  • 我是range-v3的作者,我赞同这个消息.:-) (2认同)