在C ++中返回std :: vector的有效方法


106

在函数中返回std :: vector时,要复制多少数据,以及将std :: vector放入免费存储(在堆中)并返回指针的优化有多大,即:

std::vector *f()
{
  std::vector *result = new std::vector();
  /*
    Insert elements into result
  */
  return result;
} 

比:

std::vector f()
{
  std::vector result;
  /*
    Insert elements into result
  */
  return result;
} 


3
通过引用传递向量然后将其填充到内部f怎么样?
Kiril Kirov

4
RVO是一个非常基本的优化,大多数编译器将能够随时执行。
Remus Rusanu

随着答案的不断涌入,它可能有助于您弄清您是使用C ++ 03还是C ++ 11。两个版本之间的最佳做法相差很大。
德鲁·多曼


@Kiril Kirov,我可以不把它放到函数的参数列表中吗?无效f(std :: vector&result)?
莫滕

Answers:


140

在C ++ 11中,这是首选方式:

std::vector<X> f();

也就是说,按价值回报。

在C ++ 11中,std::vector具有移动语义,这意味着函数中声明的局部向量将在返回时移动,并且在某些情况下,甚至编译器也可以忽略该移动。


13
@LeonidVolnitsky:是的,如果它是local。实际上,return std::move(v);即使使用just也会禁用移动省略return v;。因此,后者是首选。
纳瓦兹

1
@juanchopanza:我不这么认为。在C ++ 11之前,您可能会反对它,因为矢量将不会被移动。RVO是依赖于编译器的东西!谈论80年代和90年代的事情。
Nawaz

2
我对返回值(按值)的理解是:不是“被移动”,而是在调用者的堆栈上创建了被调用方中的返回值,因此被调用方中的所有操作都就位,RVO中没有任何可移动的。那是对的吗?
r0ng

2
@ r0ng:是的,这是真的。这就是编译器通常实现RVO的方式。
纳瓦兹

1
@Nawaz不是。甚至没有动静。
Lightness Races in Orbit,

70

您应该按价值返回。

该标准具有特定的功能,可以提高按价值返回的效率。它被称为“复制省略”,在这种情况下更具体地称为“命名返回值优化(NRVO)”。

编译器没有实现它,但随后又编译器不具备实现内联函数(或执行任何优化)。但是,如果编译器不进行优化,则标准库的性能可能会很差,并且所有严肃的编译器都实现内联和NRVO(以及其他优化)。

当应用NRVO时,以下代码将不会进行复制:

std::vector<int> f() {
    std::vector<int> result;
    ... populate the vector ...
    return result;
}

std::vector<int> myvec = f();

但是用户可能想要这样做:

std::vector<int> myvec;
... some time later ...
myvec = f();

复制省略在这里不会阻止复制,因为它是分配而不是初始化。但是,您仍然应该按值返回。在C ++ 11中,该赋值通过称为“移动语义”的不同方法进行了优化。在C ++ 03中,上面的代码确实会导致复制,尽管从理论上说优化程序可以避免复制,但实际上它太难了。因此myvec = f(),在C ++ 03中,您应该编写以下代码:

std::vector<int> myvec;
... some time later ...
f().swap(myvec);

还有另一个选项,可以为用户提供更灵活的界面:

template <typename OutputIterator> void f(OutputIterator it) {
    ... write elements to the iterator like this ...
    *it++ = 0;
    *it++ = 1;
}

然后,您还可以在此之上支持现有的基于矢量的接口:

std::vector<int> f() {
    std::vector<int> result;
    f(std::back_inserter(result));
    return result;
}

如果您现有的代码使用的方式比预先固定的数量更复杂,则这可能会比现有的代码效率低reserve()。但是,如果您现有的代码基本上push_back是反复调用向量,则此基于模板的代码应该同样出色。


提出了真正最好和详细的答案。但是,在您的swap()变体中(对于不带NRVO的C ++ 03),您仍然会在f()内部创建一个copy-constructor副本:从变量result到隐藏的临时对象,最后将其交换为myvec
JenyaKh

@JenyaKh:当然,这是实现质量的问题。该标准不需要C ++ 03实现实现NRVO,就像它不需要函数内联一样。与函数内联的区别在于,内联不会改变语义或程序,而NRVO会改变。可移植代码必须与NRVO一起使用,也可以不与NRVO一起使用。特定实现的优化代码(和特定的编译器标志)可以在实现自身的文档中寻求有关NRVO的保证。
史蒂夫·杰索普

3

是时候我发布关于RVO的答案了,我也是...

如果按值返回对象,则编译器通常会对此进行优化,这样就不会构造两次,因为在函数中将其构造为临时对象然后复制它是多余的。这称为返回值优化:创建的对象将被移动而不是被复制。


1

C ++ 11之前的常见用法是将引用传递给要填充的对象。

这样就不会复制该向量。

void f( std::vector & result )
{
  /*
    Insert elements into result
  */
} 

3
在C ++ 11中,这不再是成语。
纳瓦兹

1
@纳瓦兹我同意。我不确定关于C ++问题的SO最佳实践是什么,但不是专门针对C ++ 11的问题。我怀疑我应该倾向于将C ++ 11答案提供给学生,将C ++ 03答案提供给在生产代码中深waist的人。你有意见吗?
德鲁·多曼

7
实际上,在发布C ++ 11(已有19个月的历史)之后,我认为每个问题都是C ++ 11问题,​​除非明确声明是C ++ 03问题。
纳瓦兹

1

如果编译器支持命名返回值优化(http://msdn.microsoft.com/zh-cn/library/ms364057(v=vs.80).aspx),则可以在不存在以下条件的情况下直接返回向量:

  1. 不同的路径返回不同的命名对象
  2. 引入了EH状态的多个返回路径(即使在所有路径上都返回了相同的命名对象)。
  3. 返回的命名对象在嵌入式asm块中引用。

NRVO优化了冗余副本构造函数和析构函数调用,从而提高了整体性能。

您的示例中应该没有真正的差异。


0
vector<string> getseq(char * db_file)

如果要在main()上打印它,则应循环执行。

int main() {
     vector<string> str_vec = getseq(argv[1]);
     for(vector<string>::iterator it = str_vec.begin(); it != str_vec.end(); it++) {
         cout << *it << endl;
     }
}

-2

就像“按值返回”一样好,这种代码可能会导致错误。考虑以下程序:

    #include <string>
    #include <vector>
    #include <iostream>
    using namespace std;
    static std::vector<std::string> strings;
    std::vector<std::string> vecFunc(void) { return strings; };
    int main(int argc, char * argv[]){
      // set up the vector of strings to hold however
      // many strings the user provides on the command line
      for(int idx=1; (idx<argc); ++idx){
         strings.push_back(argv[idx]);
      }

      // now, iterate the strings and print them using the vector function
      // as accessor
      for(std::vector<std::string>::interator idx=vecFunc().begin(); (idx!=vecFunc().end()); ++idx){
         cout << "Addr: " << idx->c_str() << std::endl;
         cout << "Val:  " << *idx << std::endl;
      }
    return 0;
    };
  • 问:执行以上操作会发生什么?答:一个核心转储。
  • 问:为什么编译器没有发现错误?答:因为该程序在语法上是正确的,尽管在语义上不是正确的。
  • 问:如果修改vecFunc()以返回引用会怎样?答:程序运行完成并产生预期结果。
  • 问:有什么区别?答:编译器不必创建和管理匿名对象。程序员已指示编译器仅将一个对象用于迭代器和端点确定,而不是像损坏的示例那样使用两个不同的对象。

即使使用GNU g ++报告选项,上述错误程序也不会表示任何错误-Wall -Wextra -Weffc ++

如果必须产生一个值,则可以使用以下方法代替两次调用vecFunc():

   std::vector<std::string> lclvec(vecFunc());
   for(std::vector<std::string>::iterator idx=lclvec.begin(); (idx!=lclvec.end()); ++idx)...

上面的代码在循环的迭代过程中也不会产生匿名对象,但是需要进行可能的复制操作(如某些注意事项所述,在某些情况下可能会进行优化。但是引用方法保证不会产生任何复制对象。相信编译器会执行RVO不能替代尝试构建最有效的代码,如果您可以消除编译器执行RVO的需要,那么您将处于领先地位。


3
这更多是一个示例,说明如果用户通常不熟悉C ++,可能会出错。熟悉诸如.net或javascript之类的基于对象的语言的人可能会假设字符串向量始终作为指针传递,因此在您的示例中始终会指向相同的对象。vecfunc()。begin()和vecfunc()。end()在您的示例中不一定匹配,因为它们应该是字符串向量的副本。
Medran '18 -10-5

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.