Инициализация размера вектора C++

Каковы преимущества (если есть) инициализации размера C++, а также других контейнеров? Есть ли причина не просто использовать конструктор no-arg по умолчанию?

В основном, существуют ли существенные различия в производительности между

vector<Entry> phone_book;

а также

vector<Entry> phone_book(1000);

Эти примеры взяты из C++ языка программирования третьего издания от Bjarne Stroustrup. Если эти контейнеры всегда должны быть инициализированы с размером, есть ли хороший способ определить, какой хороший размер для начала?

Ответ 1

Существует несколько способов создания vector с элементами n:

ctor + по умолчанию push_back (неоптимальный)

std::vector<Entry> phone_book;
for (std::size_t i = 0; i < n; ++i) {
    phone_book.push_back(entry);
}

Это имеет тот недостаток, что при перемещении элементов происходит перераспределение. Это означает выделение памяти, перемещение элементов (или копирование, если они не могут быть перемещены или для pre c ++ 11) и освобождение памяти (с уничтожением объекта). Скорее всего, это произойдет более одного раза для n прилично большого. Стоит отметить, что для push_back гарантированная "амортизированная постоянная" означает, что он не будет перераспределяться после каждого push_back. Каждое перераспределение будет увеличивать размер геометрически. Далее читайте: стратегия перераспределения std::vector и std::string

Используйте это, если вы не знаете размер заранее, и у вас даже нет приблизительной оценки размера.

"подсчитать количество вставленных по умолчанию экземпляров T" ctor с последующими назначениями (не рекомендуется)

std::vector<Entry> phone_book(n);
for (auto& elem : phone_book) {
    elem = entry;
}

Это не требует перераспределения, но все элементы n будут изначально созданы по умолчанию, а затем скопированы для каждого нажатия. Это большой недостаток, и влияние на производительность будет измеримым. (это менее заметно для основных типов).

Не используйте это, поскольку есть лучшие альтернативы для почти каждого сценария.

cort "количество копий элементов" (рекомендуется)

std::vector<Entry> phone_book(n, entry);

Это лучший метод для использования. Поскольку вы предоставляете всю информацию, необходимую в конструкторе, он сделает наиболее эффективное распределение + назначение. Это может привести к появлению кода без ответвлений с векторизованными инструкциями для назначений, если Entry имеет тривиальный конструктор копирования.

ctor по умолчанию + reserve + push_back (рекомендуется ситуационная ситуация)

vector<Entry> phone_book;
phone_book.reserve(n);
for (std::size_t i = 0; i < n; ++i) {
     phone_book.push_back(entry);
}

Перераспределение не произойдет, и объекты будут построены только один раз. Лучшим выбором для push_back может быть emplace_back.

Используйте это, если у вас есть приблизительное приближение размера. Предпочитайте большее предположение, а не низкое), и в конце вы можете использовать shrink_to_fit.

Используйте это, если не все элементы одинаковы, например:

Entry make_entry(std::size_t i);

std::vector<Entry> phone_book;
phone_book.reserve(n);
for (std::size_t i = 0; i < n; ++i) {
     phone_book.push_back(make_entry(i));
}

стандартные ctor + std::fill_n и std::back_inserter (рекомендуется ситуационная ситуация)

#include <algorithm>
#include <iterator>

std::vector<Entry> phone_book;
std::fill_n(std::back_inserter(phone_book), n, entry);

Это в значительной степени <algorithm> версия reserve + push_back.

Используйте это, если вам нужно заполнить вектор после его определения

ctor + по умолчанию std::generate_n и std::back_inserver (для разных объектов entry)

Entry entry_generator();

std::vector<Entry> phone_book;
std::generate_n(std::back_inserter(phone_book), n, [] { return entry_generator(); });

Вы можете использовать это, если каждый entry отличается и получен из генератора, где вы не меняете параметры.

Ответ 2

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

vector<Entry> phone_book();
phone_book.reserve(1000);

// add entries dynamically at another point

phone_book.push_back(an_entry);

РЕДАКТИРОВАТЬ:

@juanchopanza делает хороший момент - если вы хотите избежать создания по умолчанию объектов, тогда зарезервируйте и используйте push_back если у вас есть конструктор перемещения или emplace_back для прямой emplace_back.

Ответ 3

Вы инициализируете размер, когда имеете представление о количестве элементов, которые нужно хранить в векторе. Если вы извлекаете данные из базы данных или другого источника, например, у вас есть 1000 элементов, то имеет смысл идти вперед и выделять вектор с внутренним массивом, который будет хранить столько данных. Если вы не знаете, какой размер будет нужен, тогда вполне возможно, что вектор будет расти по мере необходимости с течением времени.

Правильный ответ сводится к вашему заявлению и его конкретному варианту использования. Вы можете проверить производительность и настроить размер по мере необходимости. Обычно это хорошая идея, чтобы просто заставить работать, а затем вернуться и проверить последствия этих изменений позже. Много времени вы обнаружите, что настройки по умолчанию работают нормально.

Ответ 4

Это плохой пример Бьярне Страуструпа. Вместо второго определения

vector<Entry> phone_book(1000);

было бы гораздо лучше написать

vector<Entry> phone_book;
phone_book.reserve( 1000 );

Нет общих "хороших путей", чтобы определить, какой хороший размер для начала. Это зависит от информации, которую вы имеете о задаче. Но в любом случае вы можете использовать некоторое начальное распределение, если уверены, что новые элементы будут добавлены в вектор.