C ++ 'da std :: vektör döndürmenin verimli yolu


110

Ne kadar veri kopyalanır, bir işlevde bir std :: vektörü döndürülürken ve std :: vektörü serbest depoya (yığın üzerine) yerleştirmek ve bunun yerine bir işaretçi döndürmek ne kadar büyük bir optimizasyon olacaktır:

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

şundan daha verimli:

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

?


5
Vektörü referansla geçirip sonra içine doldurmaya ne dersiniz f?
Kiril Kirov

4
RVO , çoğu derleyicinin her an yapabileceği oldukça basit bir optimizasyondur.
Remus Rusanu

Cevaplar akarken, C ++ 03 mü yoksa C ++ 11 mi kullandığınızı netleştirmenize yardımcı olabilir. İki sürüm arasındaki en iyi uygulamalar oldukça farklıdır.
Drew Dormann


@Kiril Kirov, Bunu ie fonksiyonun argüman listesine koymadan yapabilir miyim? void f (std :: vektör ve sonuç)?
Morten

Yanıtlar:


152

C ++ 11'de bu tercih edilen yoldur:

std::vector<X> f();

Yani, değere göre dönüş.

C ++ 11 ile, std::vectorhareket-anlambilimine sahiptir, bu , fonksiyonunuzda bildirilen yerel vektörün dönüşte taşınacağı ve bazı durumlarda hareketin bile derleyici tarafından atlanabileceği anlamına gelir.


2
Olmasa bile taşınacak std::movemı?
Leonid Volnitsky

14
@LeonidVolnitsky: Yerel ise evet . Aslında, return std::move(v);adaletle mümkün olsa bile hamle seçimini devre dışı bırakır return v;. Bu yüzden ikincisi tercih edilir.
Nawaz

1
@juanchopanza: Sanmıyorum. C ++ 11'den önce, buna karşı çıkabilirsiniz çünkü vektör hareket etmeyecektir; ve RVO, derleyiciye bağımlı bir şeydir! 80'ler ve 90'lardan kalma şeyler hakkında konuşun.
Nawaz

2
Dönüş değeri (değere göre) hakkındaki anlayışım şudur: 'taşınmış' yerine, arayan uçtaki dönüş değeri arayanın yığınında oluşturulur, bu nedenle aranan uçtaki tüm işlemler yerinde olur, RVO'da taşınacak hiçbir şey yoktur . Bu doğru mu?
r0ng

2
@ r0ng: Evet, bu doğru. Derleyiciler genellikle RVO'yu bu şekilde uygular.
Nawaz

74

Değere göre dönmelisiniz.

Standart, değere göre geri dönüş verimliliğini artırmak için belirli bir özelliğe sahiptir. Buna "kopya elizyonu" denir ve daha özel olarak bu durumda "adlandırılmış dönüş değeri optimizasyonu (NRVO)".

Derleyiciler uygulamak gerekmez, ama sonra tekrar derleyiciler yok olması inlining işlevi uygulamak için (ya da hiç bir optimizasyon gerçekleştirmek). Ancak, derleyiciler optimize etmezse ve tüm ciddi derleyiciler satır içi ve NRVO (ve diğer optimizasyonlar) uygularsa standart kitaplıkların performansı oldukça zayıf olabilir.

NRVO uygulandığında, aşağıdaki kodda herhangi bir kopyalama olmayacaktır:

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

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

Ancak kullanıcı bunu yapmak isteyebilir:

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

Kopyalama seçimi burada bir kopyayı engellemez çünkü bu bir başlatma değil atamadır. Ancak yine de değere göre dönmelisiniz. C ++ 11'de atama, "taşıma semantiği" adı verilen farklı bir şeyle optimize edilir. C ++ 03'te, yukarıdaki kod bir kopyaya neden olur ve teoride bir eniyileyici bundan kaçınabilir, ancak pratikte çok zordur. Yani myvec = f(), C ++ 03 yerine şunu yazmalısınız:

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

Kullanıcıya daha esnek bir arayüz sunmak olan başka bir seçenek daha var:

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

Bunun üzerine mevcut vektör tabanlı arayüzü de destekleyebilirsiniz:

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

Bu belki senin kod kullanımları mevcut ise, varolan kodun daha az verimli olması reserve()ön sadece sabit bir miktardan daha fazla karmaşık bir şekilde. Ancak mevcut kodunuz temelde push_backvektörü tekrar tekrar çağırıyorsa , o zaman bu şablon tabanlı kodun o kadar iyi olması gerekir.


1
Gerçekten en iyi ve ayrıntılı cevaba oy verdi. Bununla birlikte, swap () varyantınızda ( NRVO'suz C ++ 03 için ) yine de f () içinde yapılmış bir kopya-yapıcı kopyasına sahip olacaksınız: değişken sonuçtan , en sonunda myvec ile değiştirilecek gizli bir geçici nesneye .
JenyaKh

@JenyaKh: Elbette, bu bir uygulama kalitesi sorunu. Standart, C ++ 03 uygulamalarının işlev satır içi gerektirmediği gibi NRVO uygulamasını gerektirmiyordu. Fonksiyon satır içi işleminden farkı, satır içi yapmanın anlamsallığı veya programınızı değiştirmemesi, oysa NRVO'nun yaptığıdır. Taşınabilir kod, NRVO ile veya NRVO olmadan çalışmalıdır. Belirli bir uygulama için optimize edilmiş kod (ve belirli derleyici bayrakları), uygulamanın kendi belgelerinde NRVO ile ilgili garantiler isteyebilir.
Steve Jessop

3

RVO hakkında bir cevap gönderme zamanım geldi , ben de ...

Bir nesneyi değere göre döndürürseniz, derleyici genellikle bunu en iyi duruma getirir, böylece onu işlevde geçici olarak inşa etmek ve sonra kopyalamak gereksiz olduğu için iki kez inşa edilmez. Buna dönüş değeri optimizasyonu denir: oluşturulan nesne kopyalanmak yerine taşınacaktır.


1

Yaygın bir pre-C ++ 11 deyimi, doldurulmakta olan nesneye bir başvuru iletmektir.

O zaman vektörün kopyalanması yok.

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

3
Bu artık C ++ 11'de bir deyim değil.
Nawaz

1
@Nawaz Katılıyorum. C ++ ile ilgili sorularla ilgili olarak şu anda en iyi uygulamanın ne olduğundan emin değilim, ancak özellikle C ++ 11 ile ilgili değil. Bir öğrenciye C ++ 11 yanıtları, üretim kodunda bel boyu birine C ++ 03 yanıtları verme eğiliminde olmam gerektiğini düşünüyorum. Bir fikrin var mı?
Drew Dormann

7
Aslında, C ++ 11'in (19 aylık) yayınlanmasından sonra, açıkça C ++ 03 sorusu olduğu belirtilmediği sürece her soruyu C ++ 11 sorusu olarak görüyorum.
Nawaz

1

Derleyici, Adlandırılmış Dönüş Değeri Optimizasyonunu ( http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx ) destekliyorsa , aşağıdakilerin olmaması koşuluyla doğrudan vektörü döndürebilirsiniz:

  1. Farklı adlandırılmış nesneleri döndüren farklı yollar
  2. EH durumlarıyla birlikte birden çok dönüş yolu (aynı adlandırılmış nesne tüm yollarda döndürülse bile).
  3. Döndürülen adlandırılmış nesneye bir satır içi asm bloğunda başvurulur.

NRVO, yedek kopya yapıcı ve yıkıcı çağrılarını optimize eder ve böylece genel performansı iyileştirir.

Örneğinizde gerçek bir fark olmamalıdır.


0
vector<string> getseq(char * db_file)

Ve eğer main () 'e yazdırmak istiyorsanız, bunu bir döngü içinde yapmalısınız.

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

"Değere göre dönüş" ne kadar güzel olursa olsun, bu, birini hataya götürebilecek türden bir koddur. Aşağıdaki programı düşünün:

    #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;
    };
  • S: Yukarıdakiler uygulandığında ne olacak? C: Bir karot pompası.
  • S: Derleyici neden hatayı yakalamadı? C: Çünkü program anlamsal olarak olmasa da sözdizimsel olarak doğrudur.
  • S: vecFunc () 'i bir referans döndürmek için değiştirirseniz ne olur? C: Program tamamlanana kadar çalışır ve beklenen sonucu verir.
  • S: Aralarındaki fark nedir? C: Derleyicinin anonim nesneler yaratması ve yönetmesi gerekmez. Programcı, derleyiciye, kırık örneğin yaptığı gibi iki farklı nesne yerine yineleyici ve son nokta belirleme için tam olarak bir nesne kullanması talimatını verdi.

Yukarıdaki hatalı program, GNU g ++ raporlama seçeneklerini kullansa bile hata göstermeyecektir -Wall -Wextra -Weffc ++

Bir değer üretmeniz gerekiyorsa, vecFunc () öğesini iki kez çağırmak yerine aşağıdakiler çalışır:

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

Yukarıdakiler ayrıca döngünün yinelenmesi sırasında anonim nesneler üretmez, ancak olası bir kopyalama işlemi gerektirir (bazı notlara göre, bazı durumlarda optimize edilebilir. Ancak referans yöntemi hiçbir kopyanın üretilmeyeceğini garanti eder. Derleyicinin yapacağına inanmak RVO gerçekleştirmek, yapabileceğiniz en verimli kodu oluşturmaya çalışmanın yerini tutmaz.Eğer derleyicinin RVO yapma ihtiyacını karşılayabilirseniz, oyunda öndesiniz demektir.


3
Bu, bir kullanıcı genel olarak C ++ ile aşina değilse neyin yanlış gidebileceğinin bir örneğidir. .Net veya javascript gibi nesne tabanlı dillere aşina olan biri, muhtemelen dize vektörünün her zaman bir işaretçi olarak aktarıldığını varsayacaktır ve bu nedenle sizin örneğinizde her zaman aynı nesneyi gösterecektir. vecfunc (). begin () ve vecfunc (). end (), dize vektörünün kopyaları olması gerektiğinden, örneğinizde mutlaka eşleşmeyecektir.
Medran

-2
   vector<string> func1() const
   {
      vector<string> parts;
      return vector<string>(parts.begin(),parts.end()) ;
   } 
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.