19

从函数返回 stl 向量时:

vector<int> getLargeArray() {  ...  }

退货会是昂贵的复制操作吗?我记得在某处读到向量赋值很快——我应该要求调用者传递一个引用吗?

void getLargeArray( vector<int>& vec ) {  ...  }
4

2 回答 2

24

假设您的函数构造并返回新数据,您应该按值返回,并尝试确保函数本身有一个返回点返回类型为 的变量vector<int>,或者在最坏的情况下有几个返回点都返回相同的变量。

这确保您将在任何可靠的编译器上获得命名的返回值优化,从而消除了一个潜在的副本(从函数中的值到返回值的副本)。还有其他方法可以获得返回值优化,但它不是完全可预测的,所以简单的规则很安全。

接下来,您希望消除从返回值到调用者对其进行的任何操作的潜在副本。这是调用者的问题,而不是被调用者的问题,基本上有三种方法可以做到这一点:

  • 使用对函数的调用作为 a 的初始化程序vector<int>,在这种情况下,任何可靠的 C++ 编译器都会忽略副本。
  • 使用vector具有移动语义的 C++11。
  • 在 C++03 中,使用“交换”。

也就是说,在 C++03中不要

vector<int> v;
// use v for some stuff
// ...
// now I want fresh data in v:
v = getLargeArray();

反而:

getLargeArray().swap(v);

这避免了v = getLargeArray(). 在 C++11 中不需要它,那里有一个廉价的移动分配而不是昂贵的复制分配,但当然它仍然有效。

要考虑的另一件事是您是否真的想要vector作为界面的一部分。您可以改为编写一个函数模板,该模板接受一个输出迭代器,并将数据写入该输出迭代器。想要向量中的std::back_inserter数据的调用者然后可以传入 的结果,想要数据在 a 中的调用者也可以传入dequeor list。事先知道数据大小的调用者甚至可以只传递一个向量迭代器(最好是resize()d 优先)或一个指向足够大数组的原始指针,以避免back_insert_iterator. 有做同样事情的非模板方式,但它们很可能会以一种或另一种方式产生调用开销。如果您担心复制int每个元素的成本,那么您会担心每个元素的函数调用成本。

如果你的函数不构造和返回新数据,而是返回一些现有的当前内容vector<int>并且不允许更改原始数据,那么当你按值返回时,你不能避免至少一个副本。因此,如果它的性能是一个已证明的问题,那么您需要查看一些 API,而不是按值返回。例如,您可以提供一对可用于遍历内部数据的迭代器,一个按索引在向量中查找值的函数,甚至(如果性能问题严重到需要暴露您的内部),对向量的引用。显然,在所有这些情况下,您都更改了函数的含义——现在不是给调用者“他们自己的数据”,而是提供其他人数据的视图,这可能会改变。

[*] 当然,“好像”规则仍然适用,并且可以想象一个足够聪明的 C++ 实现来实现这一点,因为这是一个可简单复制的类型 ( int) 的向量,并且因为您没有使用任何指向任何元素(我假设),然后它可以交换,结果是“好像”它被复制了。但我不会指望它。

于 2012-09-26T15:00:57.717 回答
10

您很可能会得到返回值优化,具体取决于函数体的结构。在 C++11 中,您还可以从移动语义中受益。按值返回当然具有更清晰的语义,我认为它是首选选项,除非分析证明它的成本很高。这里有一篇很好的相关文章。

这是一个带有冗长虚拟类的小示例,在没有优化或 C++11 支持的情况下使用旧版本的 GCC (4.3.4) 编译:

#include <vector>
#include <iostream>
struct Foo
{
  Foo() { std::cout << "Foo()\n"; }
  Foo(const Foo&) { std::cout << "Foo copy\n"; }
  Foo& operator=(const Foo&) { 
    std::cout << "Foo assignment\n"; 
    return *this;
  }
};

std::vector<Foo> makeFoos()
{
  std::vector<Foo> tmp;
  tmp.push_back(Foo());
  std::cout << "returning\n";
  return tmp;
}

int main()
{
  std::vector<Foo> foos = makeFoos();
}

我平台上的结果是所有复制都发生在函数返回之前。如果我使用 C++11 支持进行编译,则 push_back 会导致移动副本而不是 C++03 副本构造。

于 2012-09-26T14:51:21.667 回答