防止不必要的C ++仿函数对象副本

时间:2010-02-07 05:25:02

标签: c++ optimization templates

我有一个类,它可以累积有关一组对象的信息,并且可以充当仿函数或输出迭代器。这允许我做以下事情:

std::vector<Foo> v;
Foo const x = std::for_each(v.begin(), v.end(), Joiner<Foo>());

Foo const x = std::copy(v.begin(), v.end(), Joiner<Foo>());

现在,理论上,编译器应该能够使用copy elision and return-value optimizations,因此只需要创建一个Joiner对象。然而,在实践中,该函数会生成一个副本,然后将其复制回结果,即使在完全优化的构建中也是如此。

如果我将仿函数创建为左值,则编译器会创建两个额外的副本而不是一个:

Joiner<Foo> joiner;
Foo const x = std::copy(v.begin(), v.end(), joiner);

如果我笨拙地强制将模板类型强制转换为引用,它会传入引用,但无论如何都会复制它并返回对(现已销毁的)临时副本的悬空引用:

x = std::copy<Container::const_iterator, Joiner<Foo>&>(...));

我可以通过在std::inserter样式的仿函数中使用对状态的引用而不是状态本身来使副本变得便宜,导致类似这样的事情:

Foo output;
std::copy(v.begin(), v.end(), Joiner<Foo>(output));

但是这使得不可能使用不可变对象的“功能”风格,而且通常不那么好。

是否有某种方法可以鼓励编译器忽略临时副本,或者让它一直通过引用并返回相同的引用?

5 个答案:

答案 0 :(得分:15)

你偶然发现了一个经常抱怨<algorithm>行为的人。他们可以用仿函数做什么没有限制,所以问题的答案是否定的:没有办法鼓励编译器忽略副本。 它不是(总是)编译器,它是库实现。他们只是喜欢按值传递仿函数(想想std :: sort做一个qsort,通过值将仿函数传递给递归调用等)。

您还偶然发现了每个人都使用的确切解决方案:让仿函数保持对状态的引用,因此所有副本在需要时都指向相同的状态。

我发现这具有讽刺意味:

  

但是这使得不可能使用不可变对象的“功能”风格,而且通常不那么好。

...因为整个问题都取决于你有一个复杂的有状态函子,创建副本是有问题的。如果您使用“功能”样式的不可变对象,这将是一个非问题 - 额外的副本不会成为问题,是吗?

答案 1 :(得分:4)

如果您有最近的编译器(我认为至少是Visual Studio 2008 SP1或GCC 4.4),您可以使用std :: ref / std :: cref

#include <string>
#include <vector>
#include <functional> // for std::cref
#include <algorithm>
#include <iostream>

template <typename T>
class SuperHeavyFunctor 
{
    std::vector<char> v500mo;
    //ban copy
    SuperHeavyFunctor(const SuperHeavyFunctor&);
    SuperHeavyFunctor& operator=(const SuperHeavyFunctor&);
public:
    SuperHeavyFunctor():v500mo(500*1024*1024){}
    void operator()(const T& t) const { std::cout << t << std::endl; }
};

int main()
{
    std::vector<std::string> v; v.push_back("Hello"); v.push_back("world");
    std::for_each(v.begin(), v.end(), std::cref(SuperHeavyFunctor<std::string>()));
    return 0;
}

编辑:实际上,MSVC10的reference_wrapper实现似乎并不知道如何推导出函数对象operator()的返回类型。我必须从std::unary_function<T, void>派生SuperHeavyFunctor才能使其正常工作。

答案 2 :(得分:2)

只需快速说明, for_each 累积转换 (第二张表格),不提供订单保证在遍历所提供的范围时。

这使得实现者能够提供 sense 来提供这些函数的多线程/并发版本。

因此,算法能够提供传入的仿函数的等效实例(新副本)是合理的。

制作有状态的仿函数时要小心。

答案 3 :(得分:1)

RVO只是 - 返回值优化。今天,大多数编译器默认启用此功能。但是,参数传递返回值。您可能无法期望一个优化适合所有地方。

参见复制省略的条件,在12.8,第15段,第3项中有明确规定。

  

当一个临时类对象有   没有受到参考(12.2)   将被复制到一个类对象   相同的cv-unqualified type ,副本   操作可以省略   构造临时对象   直接进入了目标   省略副本

     

[强调我的]

LHS Fooconst合格的,暂时不合格。恕我直言,这排除了复制省略的可能性。

答案 4 :(得分:1)

对于可以使用pre-c ++ 11代码的解决方案,你可以考虑使用boost :: function和boost :: ref(如boost::reference_wrapper alone doesn't has an overloaded operator(),这与std :: reference_wrapper确实如此)。从这个页面http://www.boost.org/doc/libs/1_55_0/doc/html/function/tutorial.html#idp95780904,你可以在boost :: ref然后一个boost :: function对象中双重包装你的仿函数。我尝试了这个解决方案,它完美无缺。

对于c ++ 11,你可以使用std :: ref并且它将完成这项工作。