标准算法按值采用 Lambda 是否有原因?

Is There a Reason Standard Algorithms Take Lambdas by Value?

所以我在这里问了一个问题: 我得到了答复,我的代码是自标准 25.1 [algorithms.general] 10 以来定义的实现:

Unless otherwise specified, algorithms that take function objects as arguments are permitted to copy those function objects freely. Programmers for whom object identity is important should consider using a wrapper class that points to a noncopied implementation object such as reference_wrapper<T>

我想知道发生这种情况的原因?我们一生都被告知要通过引用来获取对象,那么为什么标准是通过值来获取函数对象,甚至在我的链接问题中更糟糕的是复制这些对象?这样做有什么我不明白的优点吗?

我不确定我是否有答案,但如果我的对象生命周期是正确的,我认为这是可移植的、安全的并且增加了零开销或复杂性:

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>


// @pre f must be an r-value reference - i.e. a temporary
template<class F>
auto resist_copies(F &&f) {
    return std::reference_wrapper<F>(f);
};

void removeIntervals(std::vector<double> &values, const std::vector<std::pair<int, int>> &intervals) {
    values.resize(distance(
            begin(values),
            std::remove_if(begin(values), end(values),
                           resist_copies([i = 0U, it = cbegin(intervals), end = cend(intervals)](const auto&) mutable 
    {
        return it != end && ++i > it->first && (i <= it->second || (++it, true));
    }))));
}


int main(int argc, char **args) {
    // Intervals of indices I have to remove from values
    std::vector<std::pair<int, int>> intervals = {{1,  3},
                                                  {7,  9},
                                                  {13, 13}};

    // Vector of arbitrary values.
    std::vector<double> values = {4.2, 6.4, 2.3, 3.4, 9.1, 2.3, 0.6, 1.2, 0.3, 0.4, 6.4, 3.6, 1.4, 2.5, 7.5};
    removeIntervals(values, intervals);
    // intervals should contain 4.2,9.1,2.3,0.6,6.4,3.6,1.4,7.5

    std:
    copy(values.begin(), values.end(), std::ostream_iterator<double>(std::cout, ", "));
    std::cout << '\n';
}

std 假定函数对象和迭代器可以自由复制。

std::ref 提供了一种将函数对象转换为具有兼容 operator() 的伪引用的方法,该方法使用引用而不是值语义。所以没有大的价值丢失。

如果您一生都被教导通过引用获取对象,请重新考虑。除非有充分的理由,否则按值获取对象。关于价值的推理要容易得多;引用是指向程序中任何位置的任何状态的指针。

引用的常规用法,作为指向本地对象的指针,在使用它的上下文中没有被任何其他活动引用引用,阅读您的代码的人和编译器都无法假定。如果您以这种方式推理引用,它们不会为您的代码增加可笑的复杂性。

但是,如果您以这种方式推理它们,那么当您的假设被违反时,您将遇到错误,它们将是微妙的、粗暴的、意想不到的和可怕的。

一个典型的例子是当 this 和参数引用同一个对象时中断的 operator= 的数量。但是任何接受两个相同类型的引用或指针的函数都有同样的问题。

但即使是一个引用也会破坏您的代码。让我们看看sort。在伪代码中:

void sort( Iterator start, Iterator end, Ordering order )

现在,让我们将订购作为参考:

void sort( Iterator start, Iterator end, Ordering const& order )

这个怎么样?

std::function< void(int, int) > alice;
std::function< void(int, int) > bob;
alice = [&]( int x, int y ) { std:swap(alice, bob); return x<y; };
bob = [&]( int x, int y ) { std:swap(alice, bob); return x>y; };

现在,打电话给sort( begin(vector), end(vector), alice )

每次调用 < 时,引用的 order 对象都会交换含义。现在这很荒谬,但是当您将 Ordering 取为 const& 时,优化器必须考虑这种可能性并在每次调用您的订购代码时将其排除!

您不会执行上述操作(实际上这个特定的实现是 UB,因为它会违反 std::sort 上的任何合理要求);但编译器必须证明你没有在每次跟随 order 或调用它时做某事 "like that"(更改 ordering 中的代码)!这意味着不断重新加载 order 的状态,或者内联并证明你没有这样的疯狂。

在按值取值时执行此操作要困难一个数量级(并且基本上需要 std::ref 之类的东西)。优化器有一个函数对象,它是本地的,它的状态也是本地的。存储在其中的任何内容都是本地的,编译器和优化器知道谁可以合法修改它。

你编写的每一个函数都采用 const& 并且离开它的 "local scope"(比如,称为 C 库函数)不能假设 const& 的状态在之后保持不变它回来了。它必须从指针指向的任何地方重新加载数据。

现在,我确实说过按值传递,除非有充分的理由。并且有很多充分的理由;例如,您的类型移动或复制非常昂贵,这是一个很好的理由。您正在向其中写入数据。您实际上希望它在您每次阅读时都发生变化。等等

但默认行为应该是按值传递。只有在有充分理由的情况下才转向参考文献,因为成本是分散的并且很难确定。