簡體   English   中英

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

[英]Efficient way to return a std::vector in c++

在 function 中返回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;
} 

?

在 C++11 中,這是首選方式:

std::vector<X> f();

即按值返回。

在 C++11 中, std::vector具有移動語義,這意味着函數中聲明的局部向量將在返回時移動,在某些情況下,編譯器甚至可以忽略移動。

您應該按值返回。

該標准具有提高按值返回效率的特定功能。 它被稱為“復制省略”,在這種情況下更具體地說是“命名返回值優化(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 中,上面的代碼確實會導致復制,雖然理論上優化器可以避免它,但實際上它太難了。 因此,在 C++03 中,您應該這樣寫,而不是myvec = f()

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 ,那么這個基於模板的代碼應該也一樣好。

是時候發布關於RVO的答案了,我也是......

如果您按值返回一個對象,編譯器通常會優化它,因此它不會被構造兩次,因為在函數中構造它作為臨時對象然后復制它是多余的。 這稱為返回值優化:創建的對象將被移動而不是被復制。

如果編譯器支持命名返回值優化 ( http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx ),則可以直接返回向量,前提是沒有:

  1. 不同的路徑返回不同的命名對象
  2. 引入了 EH 狀態的多個返回路徑(即使在所有路徑上都返回了相同的命名對象)。
  3. 返回的命名對象在內聯 asm 塊中被引用。

NRVO 優化了冗余的復制構造函數和析構函數調用,從而提高了整體性能。

在您的示例中應該沒有真正的差異。

一個常見的 C++11 之前的習慣用法是傳遞對正在填充的對象的引用。

那么就沒有向量的復制。

void f( std::vector & result )
{
  /*
    Insert elements into result
  */
} 
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;
     }
}

是的,按價值回報。 編譯器可以自動處理它。

以下代碼將在沒有復制構造函數的情況下工作:

你的日常:

std::vector<unsigned char> foo()
{
    std::vector<unsigned char> v;
    v.resize(16, 0);

    return std::move(v); // move the vector
}

之后,您可以使用 foo 例程獲取向量而不復制自身:

std::vector<unsigned char>&& moved_v(foo()); // use move constructor

結果:moved_v 大小為 16,並由 [0] 填充

就像“按值返回”一樣好,它是一種可能導致錯誤的代碼。 考慮以下程序:

    #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;
    };
  • 問:執行上述操作后會發生什么? 答:核心轉儲。
  • 問:為什么編譯器沒有發現錯誤? A:因為該程序在語法上是正確的,盡管在語義上是正確的。
  • 問:如果修改 vecFunc() 以返回引用會發生什么? A:程序運行完成並產生預期的結果。
  • 問:有什么區別? 答:編譯器不必創建和管理匿名對象。 程序員已指示編譯器為迭代器和端點確定使用一個對象,而不是像損壞的示例那樣使用兩個不同的對象。

即使使用 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 的需要,那么您就領先了。

   vector<string> func1() const
   {
      vector<string> parts;
      return vector<string>(parts.begin(),parts.end()) ;
   } 

這在 c++11 之后仍然有效,因為編譯器自動使用移動而不是復制。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM