Эффективный способ вернуть std :: vector в c ++

108

Сколько данных копируется при возврате 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;
} 

?

Мортен
источник
4
Как насчет передачи вектора по ссылке, а затем его заполнения внутри f?
Кирилл Киров
4
RVO - это довольно простая оптимизация, которую большинство компиляторов может выполнить в любой момент.
Ремус Русану
По мере поступления ответов это может помочь вам уточнить, используете ли вы C ++ 03 или C ++ 11. Лучшие практики между двумя версиями немного различаются.
Дрю Дорманн,
@Kiril Kirov, Могу я сделать это, не помещая его в список аргументов функции, т.е. void f (std :: vector & result)?
Мортен

Ответы:

141

В C ++ 11 это предпочтительный способ:

std::vector<X> f();

То есть возврат по значению.

В C ++ 11 std::vectorесть семантика перемещения, что означает, что локальный вектор, объявленный в вашей функции, будет перемещен при возврате, а в некоторых случаях даже перемещение может быть исключено компилятором.

Наваз
источник
13
@LeonidVolnitsky: Да, если он местный . Фактически, return std::move(v);отключит перемещение-исключение, даже если это было возможно с помощью just return v;. Так что последнее предпочтительнее.
Nawaz
1
@juanchopanza: Я так не думаю. До C ++ 11 вы могли возразить против этого, потому что вектор не будет перемещен; а RVO зависит от компилятора! Поговорим о вещах из 80-х и 90-х годов.
Nawaz
2
Насколько я понимаю, возвращаемое значение (по значению) таково: вместо «перемещено» возвращаемое значение вызываемого объекта создается в стеке вызывающего объекта, поэтому все операции вызываемого объекта выполняются на месте, в RVO перемещать нечего. . Это правильно?
r0ng 05
2
@ r0ng: Да, это правда. Именно так компиляторы обычно реализуют RVO.
Nawaz
1
@ Nawaz Это не так. Нет больше даже движения.
Гонки
71

Вы должны вернуться по стоимости.

В стандарте есть особенность для повышения эффективности возврата по значению. Это называется «копированием», а конкретнее в данном случае «оптимизацией именованного возвращаемого значения (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 () ( для C ++ 03 без NRVO ) у вас по-прежнему будет одна копия конструктора копирования, сделанная внутри f (): из переменной result в скрытый временный объект, который, наконец, будет заменен на myvec .
JenyaKh 06
@JenyaKh: конечно, это проблема качества реализации. Стандарт не требовал, чтобы реализации C ++ 03 реализовывали NRVO, как и не требовал встраивания функций. Отличие от встраивания функций в том, что встраивание не меняет семантику или вашу программу, в то время как NRVO меняет. Переносимый код должен работать с NRVO или без него. Оптимизированный код для конкретной реализации (и определенных флагов компилятора) может искать гарантии относительно NRVO в собственной документации реализации.
Стив Джессоп
3

Пора ответить про RVO , я тоже ...

Если вы возвращаете объект по значению, компилятор часто оптимизирует его, чтобы он не создавался дважды, поскольку излишне создавать его в функции как временное, а затем копировать. Это называется оптимизацией возвращаемого значения: созданный объект будет перемещен, а не скопирован.


источник
1

Распространенная идиома до C ++ 11 - передавать ссылку на заполняемый объект.

Тогда нет копирования вектора.

void f( std::vector & result )
{
  /*
    Insert elements into result
  */
} 
Дрю Дорманн
источник
3
Это больше не идиома в C ++ 11.
Nawaz
1
@Nawaz Я согласен. Я не уверен, какова сейчас лучшая практика в SO относительно вопросов по C ++, но не конкретно C ++ 11. Я подозреваю, что мне следовало бы дать ответы на C ++ 11 студенту, C ++ 03 - на ответы кому-то, кто по пояс в производственном коде. У вас есть мнение?
Дрю Дорманн,
7
Фактически, после выпуска C ++ 11 (которому 19 месяцев) я считаю каждый вопрос вопросом C ++ 11, если только он явно не указан как вопрос C ++ 03.
Nawaz
1

Если компилятор поддерживает оптимизацию именованных возвращаемых значений ( http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx ), вы можете напрямую вернуть вектор при условии, что нет:

  1. Различные пути возвращают разные именованные объекты
  2. Множественные пути возврата (даже если один и тот же именованный объект возвращается на всех путях) с введенными состояниями EH.
  3. На возвращаемый именованный объект имеется ссылка во встроенном блоке asm.

NRVO оптимизирует вызовы конструктора избыточной копии и деструктора и, таким образом, улучшает общую производительность.

В вашем примере не должно быть реальной разницы.

Taocp
источник
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;
    };
  • В: Что произойдет, когда все будет выполнено? A: Заливка.
  • В: Почему компилятор не обнаружил ошибку? A: Потому что программа синтаксически, хотя и не семантически, верна.
  • В: Что произойдет, если вы измените vecFunc () для возврата ссылки? О: Программа работает до конца и дает ожидаемый результат.
  • Q: В чем разница? О: Компилятору не нужно создавать анонимные объекты и управлять ими. Программист проинструктировал компилятор использовать только один объект для итератора и для определения конечной точки, а не два разных объекта, как это делает сломанный пример.

Вышеупомянутая ошибочная программа не будет указывать на ошибки, даже если вы используете параметры отчетности 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
-2
   vector<string> func1() const
   {
      vector<string> parts;
      return vector<string>(parts.begin(),parts.end()) ;
   } 
Амрут А
источник