Ефективний спосіб повернути std :: vector в c ++


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?
Кирило Кіров

4
RVO є досить базовою оптимізацією, яку більшість компіляторів зможе зробити в будь-який момент.
Ремус Русану,

Коли відповіді надходять, це може допомогти вам пояснити, чи використовуєте ви C ++ 03 чи C ++ 11. Найкращі практики між двома версіями досить різняться.
Drew Dormann,


@Kiril Kirov, Чи можу я це зробити, не внісши це в список аргументів функції, тобто. порожнеча f (std :: vector & result)?
Мортен,

Відповіді:


140

У C ++ 11 це кращий спосіб:

std::vector<X> f();

Тобто повернення за значенням.

У C ++ 11 std::vectorмає семантику переміщення, що означає, що локальний вектор, оголошений у вашій функції, буде переміщений після повернення, а в деяких випадках компілятор може навіть вилучити цей хід.


13
@ ЛеонідВольницький: Так, якщо це місцево . Насправді, return std::move(v);вимкне рух-елізію, навіть якщо це було можливо лише за допомогою return v;. Тож останній є кращим.
Наваз,

1
@juanchopanza: Я не думаю. До C ++ 11 ви могли б заперечити проти, оскільки вектор не буде переміщений; а RVO - річ, що залежить від компілятора! Поговоримо про речі з 80-х та 90-х.
Наваз

2
Я розумію повернене значення (за значенням): замість "було переміщено", повертане значення у виклику створюється у стеку абонента, тому всі операції в виклику є на місці, у RVO немає чого рухатись . Це правильно?
r0ng

2
@ r0ng: Так, це правда. Саме так зазвичай компілятори реалізують RVO.
Nawaz

1
@Nawaz Це не так. Більше немає навіть переїзду.
Гонки легкості на орбіті

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();

Copy elision не заважає копіювати тут, оскільки це призначення, а не ініціалізація. Однак вам все одно варто повернутись за вартістю. У 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 () ( для C ++ 03 без NRVO ) ви все одно матимете одну копію конструктора копіювання, зроблену всередині f (): від змінної результату до прихованого тимчасового об'єкта, який нарешті буде замінено на myvec .
JenyaKh

@JenyaKh: звичайно, це питання якості реалізації. Стандарт не вимагав, щоб реалізації C ++ 03 реалізовували NRVO, як і не потрібно вбудовувати функції. Відмінність від вбудовування функцій полягає в тому, що вбудовування не змінює семантику або вашу програму, тоді як NRVO це робить. Портативний код повинен працювати з NRVO або без нього. Оптимізований код для конкретної реалізації (і конкретних прапорів компілятора) може шукати гарантій щодо NRVO у власній документації реалізації.
Steve Jessop

3

Пора опублікувати відповідь про RVO , я теж ...

Якщо ви повертаєте об'єкт за значенням, компілятор часто оптимізує це, щоб він не будувався двічі, оскільки зайвим буде побудувати його у функції як тимчасовий, а потім скопіювати. Це називається оптимізацією поверненого значення: створений об’єкт буде переміщено, а не скопійовано.


1

Загальна ідіома, що передує C ++ 11, - це передавання посилання на об'єкт, який заповнюється.

Тоді копіювання вектора не відбувається.

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

3
Це вже не ідіома в C ++ 11.
Наваз,

1
@Nawaz Я згоден. Я не впевнений, яка найкраща практика зараз стосується SO щодо питань щодо C ++, але не конкретно C ++ 11. Я підозрюю, що я маю схильність давати відповіді на C ++ 11 студенту, C ++ 03 - відповіді комусь по пояс у виробничому коді. У вас є думка?
Дрю Дорманн

7
Насправді, після випуску C ++ 11 (якому 19 місяців), я вважаю кожне запитання питанням C ++ 11, якщо це явно не вказано як запитання C ++ 03.
Наваз

1

Якщо компілятор підтримує іменовану оптимізацію зворотного значення ( http://msdn.microsoft.com/en-us/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 () не обов'язково збігатимуться у вашому прикладі, оскільки вони повинні бути копіями рядкового вектора.
Медран

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.