通过引用将函数对象传递给std算法
问题内容:
通过转发引用而不是按值传递函数对象到STL算法中更好吗?它将允许利用operator ()
传递的功能对象的引用限定符。
有几个约问题std::for_each
算法SO(1,2),这是考虑与传递给函数对象的可观察到的状态的改变的问题std::for_each
。
即使是对于不能返回功能对象的算法(由于它们应返回输出迭代器的最后一个值或其他值),对于那些不能返回功能对象的算法,通过左值引用传递功能对象也可以解决该问题。
例如,std::for_each
可以将算法更改为(从libc ++复制):
template<typename _InputIterator, typename _Function>
_Function
for_each(_InputIterator __first, _InputIterator __last, _Function __f)
{
for (; __first != __last; ++__first)
__f(*__first);
return _GLIBCXX_MOVE(__f);
}
至:
template<typename _InputIterator, typename _Function>
_Function &&
for_each(_InputIterator __first, _InputIterator __last, _Function && __f)
{
for (; __first != __last; ++__first)
_GLIBCXX_FORWARD(_Function, __f)(*__first);
return _GLIBCXX_FORWARD(_Function, __f);
}
或者(如果允许此类重大更改)std::for_each
可以返回void
而不会损失功能。std::forward
对于所有其余<numeric>
的和<algorithm>
算法,都可以进行类似的更改(从按值传递更改为通过转发引用传递,并将所有调用更改为调用ed函数对象,而不仅仅是非const-
lvalue)。
我知道部分解决方法:将对象传递给std::ref
(或std::cref
强制执行const this
)包装,但是将operator ()
cv-
ref-qualifiers从包装的功能对象转发到包装的对象存在问题operator ()
。
另一个解决方法是在alorithm的模板参数列表中显式指定引用参数类型,但当前Function
参数可悲的是始终遵循列表中的InputIterator
和OutputIterator
参数:
#include <iostream>
#include <list>
#include <algorithm>
#include <iterator>
#include <utility>
#include <cstdlib>
int main()
{
std::list< int > l{{1, 2, 3, 4}};
std::copy_n(std::cbegin(l), l.size(), std::ostream_iterator< int >(std::cout, " "));
std::cout << std::endl;
struct F
{
int state;
void operator () (int i) { state += i; }
} f{0};
using I = std::list< int >::const_iterator;
std::for_each< I, F && >(std::cbegin(l), std::cend(l), std::move(f));
std::cout << f.state << std::endl;
return EXIT_SUCCESS;
}
顺便说一下,更改将允许将不可复制和/或不可移动的函数对象传递给不包装它们的算法。
问题答案:
通过转发引用而不是按值传递函数对象到STL算法中更好吗?
是的,这样会更好。如果有必要要求函子不必为,或CopyConstructible
,那会更好。但是该标准在25.1中明确指出:CopyAssignable``MoveConstructible``MoveAssignable
注意:
除非另有说明,否则将以函数对象作为参数的算法允许自由复制那些函数对象。对于对象标识很重要的程序员,应考虑使用包装类,该包装类指向未复制的实现对象,例如reference_wrapper<T>
(20.9.4)或某些等效解决方案。
—尾注 ]
早在1998年就将这个问题称为LWG
92。当时添加了上面引用的“注释我”(此注释由于reference_wrapper<T>
当时不存在而被修改)。
对于std ::
lib的供应商来说,这是一个很好的解决方案,对于负责修订规范的委员会成员来说,这是一个很好的解决方案,但是对于像您这样的想要使用有状态函子的人来说,这并不是一个好选择。
当然,当时还没有转发参考作为可能的解决方案。同样在那时,std :: implementation 在
算法中按值传递函子是很常见的,这将进一步破坏其状态(如LWG 92的说明所示)。
您已正确触及与此问题有关的所有要点:
-
客户可以
std::ref
代替使用,但这不会尊重参考限定的函子。 -
客户端可以显式指定函子参考参数,但这不会禁止实现 在 算法 内 复制函子。
-
对于客户端而言,明确指定函子参考参数非常不方便,因为它们始终在模板参数列表中排在最后。
首先,libc ++是唯一编写的std :: implementation,它禁止内部复制函子。即,如果您编写LWG
92示例:
#include <algorithm>
#include <iostream>
#include <list>
#include <numeric>
template <class C>
void
display(const C& c)
{
std::cout << '{';
if (!c.empty())
{
auto i = c.begin();
std::cout << *i;
for (++i; i != c.end(); ++i)
std::cout << ", " << *i;
}
std::cout << '}' << '\n';
}
class Nth { // function object that returns true for the nth element
private:
int nth; // element to return true for
int count; // element counter
public:
Nth (int n) : nth(n), count(0) {
}
bool operator() (int) {
return ++count == nth;
}
};
int
main()
{
std::list<int> coll(10);
std::iota(coll.begin(), coll.end(), 0);
display(coll);
auto pos = std::remove_if(coll.begin(), coll.end(), Nth{3});
coll.erase(pos, coll.end());
display(coll);
}
今天的结果是:
libc ++
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
{0, 1, 3, 4, 5, 6, 7, 8, 9}
g ++
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
{0, 1, 3, 4, 6, 7, 8, 9}
VS-2015
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
{0, 1, 3, 4, 6, 7, 8, 9}
g 的libstdc 和VS-2015 仍在Nth
内部复制,remove_if
就像18年前Nico Josuttis所描述的那样。
将代码更改为:
Nth pred{3};
auto pos = std::remove_if(coll.begin(), coll.end(), std::ref(pred));
确实将结果更改为:
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
{0, 1, 3, 4, 5, 6, 7, 8, 9}
恕我直言,这只是一个运行时错误,等待不熟悉std :: lib悠久历史的程序员来进行。