为啥 std::find_if(first, last, p) 不采用引用谓词?

Posted

技术标签:

【中文标题】为啥 std::find_if(first, last, p) 不采用引用谓词?【英文标题】:Why does std::find_if(first, last, p) not take predicate by reference?为什么 std::find_if(first, last, p) 不采用引用谓词? 【发布时间】:2018-07-26 00:20:36 【问题描述】:

我查看了std::find_if on cppreference.com, 的各种签名,我注意到采用谓词函数的风格似乎按值接受它:

template< class InputIt, class UnaryPredicate >
InputIt find_if( InputIt first, InputIt last,
             UnaryPredicate p );

如果我对它们的理解正确,带有捕获变量的 lambda 会为其数据的引用或副本分配存储空间,因此“按值传递”可能意味着为调用复制捕获数据的副本。

另一方面,对于函数指针和其他可直接寻址的东西,如果直接传递函数指针,而不是通过引用到指针(pointer-to-pointer),性能应该会更好。

首先,这是正确的吗?上面的UnaryPredicate 会是按值参数吗?

其次,我对传递 lambda 的理解是否正确?

第三,在这种情况下是否有理由通过值而不是引用传递?更重要的是,是否没有一些足够模糊的语法(你好,通用引用)可以让编译器做任何它想要获得最大性能的事情?

【问题讨论】:

如果要存储谓词的副本,按引用传递可能不如按值传递最佳。但是你问的是传递,还是存储? @juanchopanza 我认为这个问题是关于一般效率的,所以你可以推出你的答案。 Passing functor object by value vs by reference (C++)的可能重复 【参考方案1】:

上面的 UnaryPredicate 会是按值参数吗?

是的,函数参数列表中就是这么写的。它接受推导的值类型。

除此之外,lambda 表达式是纯右值。这意味着,c++17 保证复制省略,p 是从 lambda 表达式直接初始化的。将闭包或捕获的对象传递给函数时不会生成额外的副本(但是,函数可能会在内部生成更多副本,尽管这并不常见)。

如果谓词是通过引用传递的,则需要物化一个临时对象。因此,对于 lambda 表达式,通过引用传递的开关不会获得任何东西。

如果您有其他种类的谓词,它们可以复制,那么您可以将std::reference_wrapper 传递给该谓词对象,以获得便宜的“句柄”。包装器的operator() 会做正确的事情。

这个定义大部分是历史性的,但现在通过值传递确实不是问题。


为了详细说明为什么引用语义会很糟糕,让我们试着经历这些年。一个简单的左值引用是行不通的,因为现在我们不支持绑定到右值。一个 const 左值引用也不行,因为现在我们要求谓词不修改任何内部状态,这是为了什么?

所以直到c++11,我们真的没有其他选择。按值传递会比引用更好。有了新标准,我们可能会修改我们的方法。为了支持右值,我们可以添加一个右值引用重载。但这是一种冗余练习,因为它不需要做任何不同的事情。

通过传递一个值,调用者可以选择如何创建它,对于纯右值,在c++17 中,它实际上是免费的。如果调用者愿意,他们可以显式地提供引用语义。所以没有任何损失,而且我认为在使用简单性和 API 设计方面收获很大。

【讨论】:

那么,为什么不让它引用参数,为什么要强制人们使用reference_wrapper?有什么收获? @Yola - 历史原因?鼓励人们做“轻”谓词?任你选。无论如何,现在没有充分的理由更改该函数签名。 @Yola 历史原因和引用语义很糟糕。将“默认”设置为值语义并强制用户做一些特殊的事情来获得引用语义是正确的方法。 我正在研究find_if 的VS17 实现,它使用具有以下签名template&lt;class _InIt, class _Pr&gt; inline _InIt _Find_if_unchecked(_InIt _First, _InIt _Last, _Pr&amp; _Pred) 的内部函数,为什么它需要通过引用传递到下一个级别? @Yola - 可能是因为它希望避免在内部制作更多副本。仅仅因为他们不能'并不意味着他们想要悲观。公共 API 发送有关引用语义的明确消息。内部 API 可以为所欲为。【参考方案2】:

其实有多种原因:

    您始终可以将推导的值参数转换为使用引用语义,但反之则不行:只需传递 std::ref(x) 而不是 xstd::reference_wrapper&lt;T&gt; 并不完全等同于传递引用,但特别是对于函数对象,它做了正确的事情。也就是说,按值传递通用参数是更通用的方法。

    通过引用传递 (T&amp;) 不适用于临时或 const 对象,T const&amp; 不适用于非const&amp;,即唯一的选择是T&amp;&amp; (转发参考)在 C++11 之前不存在,算法接口自 C++98 引入以来没有改变。

    与任何类型的引用参数(包括转发引用)不同,值参数可以复制省略。

【讨论】:

以上是关于为啥 std::find_if(first, last, p) 不采用引用谓词?的主要内容,如果未能解决你的问题,请参考以下文章

C++ CppCheck算法建议(std::find_if代替原始循环)相关性

使用std::find_if提取序列容器的子串

地图上的 find_if 问题

find_if 中具有多个参数的 Lambda

为啥 SASS 不能像 `&::not(:first-child)` 那样编译 pseudo_expr?

从地图容器中查找大于用户指定值的第一个值