为什么ranges::for_each返回函数?

20
遗留的`std::for_each`函数返回函数,因为标准只要求`Function`满足Cpp17MoveConstructible,根据[alg.foreach]
template<class InputIterator, class Function>
  constexpr Function for_each(InputIterator first, InputIterator last, Function f);

Preconditions: Function meets the Cpp17MoveConstructible requirements.

[Note: Function need not meet the requirements of Cpp17CopyConstructible. end note]

这是合理的,因为用户可能希望在调用后重复使用该函数。 for_each的并行版本没有返回值。
template<class ExecutionPolicy, class ForwardIterator, class Function>
  void for_each(ExecutionPolicy&& exec,
                ForwardIterator first, ForwardIterator last,
                Function f);

Preconditions: Function meets the Cpp17CopyConstructible requirements.

这是因为标准要求Function满足Cpp17CopyConstructible,所以返回函数是不必要的,因为用户可以在调用端自由地创建副本。
我注意到ranges::for_each也返回函数:
template<input_iterator I, sentinel_for<I> S, class Proj = identity,
         indirectly_unary_invocable<projected<I, Proj>> Fun>
  constexpr ranges::for_each_result<I, Fun>
    ranges::for_each(I first, S last, Fun f, Proj proj = {});
然而,函数签名已经要求Fun满足indirectly_unary_invocable,这已经保证了它是可复制的。
问题是,为什么ranges::for_each仍然返回函数?这样做有什么意义呢?

1
有趣的问题。可能只是为了让它更容易作为std::for_each的替代品插入进来。但这只是纯粹的猜测。 - undefined
3个回答

25
它返回函数对象,因为在过去(我假设是在C++98中)这样做可以使用一些巧妙的技巧来处理有状态的函数对象。如今很少见到这种情况,因为使用lambda表达式通常更加直接简单。
以下是一个示例:
#include <algorithm>
#include <iostream>

struct EvenCounter
{
    int count;

    EvenCounter() : count(0) {}

    void operator()(int x)
    {
        if (x % 2 == 0)
            count++;
    }
};

int main()
{
    int array[] = {1,2,3,4,5};
    int num_even = std::for_each(array, array+5, EvenCounter()).count;
    std::cout << num_even << '\n';
}

这是合理的,因为用户可能在调用后想要重用该函数。
我认为这里的逻辑是相反的。函数不需要可复制,仅仅是因为 for_each 没有理由去复制它。
如果你有一个不可复制(甚至不可移动)的函数,你可以使用 std::ref 通过引用传递它,以避免复制/移动,所以你在这里从算法返回函数并没有获得任何好处。
在 C++98 中没有 std::ref,但也没有移动语义,所以 for_each 本来就无法与不可复制的函数对象一起使用。

谢谢你的猜测。然而,我不认为有必要在现代C++中特别允许这种欺骗行为,尤其是对于更强大的C++20约束函数。请注意,ranges::for_each不仅仅返回fun,所以在你的情况下拼写应该是ranges::for_each(...).fun.count,这甚至更没有意义。 - undefined
@康桓瑋 我猜有些人仍然喜欢这个把戏,或者至少委员会成员是这样认为的。 - undefined

12
问题是,为什么ranges::for_each仍然返回函数?这样做有什么意义呢?
答案很简单:所有的std::ranges::meow算法要么返回与std::meow相同的东西(例如countfind等),要么在返回时还会额外返回新的输入迭代器(例如copy),如果这对于返回是有用的话。从技术上讲,一些返回值的函数(比如,再次提到的count)也有可能从返回结束迭代器中受益,但这对于这些函数的人机工程学来说是一个很大的牺牲,所以没有这样做。
值得注意的是,它们要么保留现有的返回类型,要么在现有的返回类型上进行扩展。
没有人愿意在已经有大量工作的基础上去改变算法的返回类型,并做出额外的设计决策。正如我们最喜欢的猫所指出的那样,在C++11中,std::for_each返回函数对象本身已经有点多余,因为如果你真的想要实现有状态的功能,只需简单地使用std::ref即可。但是,废弃这一点并将std::for_each更改为返回void似乎并不值得,而且将std::ranges::for_each作为唯一一个与其std::对应算法行为不同的算法,似乎也不值得。

嗯,严格来说不是唯一的。 std::ranges::copy 指定了在执行 std::copy 时发生了多少次迭代器增量。今年我在我的 CppNow 演讲中稍微提到了这个问题:take(5)


"...而且加倍地,std::ranges::for_each似乎不值得成为唯一一个与其std::对应算法行为不同的算法。"我想知道为什么他们对这种行为差异感到如此不情愿,当设计概念形式的类型特征对应物时(例如:std::assignable_from<std::string&, char>),他们似乎并没有这个问题。" - undefined
@303 这两件事情没有任何关系?事实证明,人们可以对不同的问题做出不同的决策。 - undefined

3
这是一个简单的用例:
struct sum {
  int total;
  auto operator ()(const int x) { total += x ; }
};

int main() {
  const auto v = std::vector{ 1, 2, 3, };
  const auto [i, f] = std::ranges::for_each(v, sum());
  std::cout << f.total;
}

编译器探索者
NB:我注意到当我写完这篇文章时,已经有一个类似的答案了...

我目前正在寻找这个选择背后的实际原因,当我从可靠的来源得到答案后,我会更新这篇文章。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接