Удобным способом инициализации контейнеров STL является использование списка инициализаторов, как показано здесь:
auto data = std::vector<std::string>{"example", "input", "data"};
Есть оговорка: аргументы конструктора вектора сначала конструируются, а затем копируются. Это свойство std::initializer_list
. Это может стать проблемой, когда параметры не являются тривиально копируемыми, поскольку они обрабатывают некоторые ресурсы и т.д. Короткие строки не являются проблемой, так как они должны обрабатываться оптимизацией для маленьких строк, но длинные (зависит от реализации стандартной библиотеки, какие из них маленькие, а какие нет) потребуют три выделения памяти: одно при создании строки, второе для элемента вектора и третье для копирования.
auto data = std::vector<std::string>{
"long string that will likely be allocated on the heap"
}; // three memory allocations
Напишем тестовую программу, которая отслеживает количество выделений памяти:
void* operator new (std::size_t count) {
std::cout << "new " << count << " bytesn";
return std::malloc(count);
}
int main() {
std::cout << "Vector with small string:n";
auto data = std::vector<std::string>{"small string"};
std::cout << "nVector with long string:n";
data = std::vector<std::string>{
"long string that will likely be allocated on the heap"
};
}
Я скомпилировал ее с помощью g++ 9.4.0 с включенными оптимизациями: g++ test.cpp --std=c++17 -O2 -o test
. Результат выполнения программы таков:
./test
Vector with small string:
new 32 bytes
Vector with long string:
new 56 bytes
new 32 bytes
new 56 bytes
Здесь видно, что для небольшой строки std::vector
выделил 32 байта, что является размером std::string
. Создание вектора для работы с длинной строкой включает в себя 3 выделения памяти: первое происходит при построении параметра, второе — это выделение памяти для std::string
внутри вектора и последнее — это копия нашего параметра.
Чтобы избавиться от копии, мы должны избегать std::initializer_list
. Давайте вместо этого напишем функцию make_vector
. Мой проект выглядит следующим образом:
template <typename T, typename... U>
std::vector<std::decay_t<T>> make_vector(T&& arg, U&&... args) {
auto result = std::vector<std::decay_t<T>>{};
result.reserve(1 + sizeof...(args));
result.emplace_back(std::forward<T>(arg));
(result.emplace_back(std::forward<U>(args)), ...);
return result;
}
Теперь параметры не будут копироваться, потому что они передаются методу emplace_back
, который конструирует их на месте. Важно выделить память для всех параметров сразу, используя метод reserve
, иначе мы сэкономим выделения на копировании, но добавим лишние выделения для элементов вектора.
Давайте настроим тестовую программу:
int main() {
std::cout << "String size: " << sizeof(std::string) << "n";
std::cout << "nVector with small string:n";
auto data = std::vector<std::string>{"short vector"};
std::cout << "nVector with a long string:n";
data = std::vector<std::string>{
"a long string that will likely be allocated on the heap"};
using namespace std::literals;
std::cout << "nmake_vector with a long string:n";
data = make_vector(
"a long string that will likely be allocated on the heap"s);
std::cout << "nVector with two long strings:n";
data = std::vector<std::string>{
"a long string that will likely be allocated on the heap",
"another long string that will likely be allocated on the heap"};
std::cout << "nmake_vector, two long strings:n";
data = make_vector(
"a long string that will likely be allocated on the heap"s,
"another long string that will likely be allocated on the heap"s);
}
Теперь вывод выглядит лучше, я добавил комментарии для пояснения:
String size: 32
Vector with small string:
new 32 bytes # vector item (sizeof std::string)
Vector with a long string:
new 56 bytes # the argument
new 32 bytes # the vector item
new 56 bytes # argument copy
make_vector with a long string:
new 56 bytes # the string argument
new 32 bytes # the vector item
Vector with two long strings:
new 56 bytes # the first argument
new 62 bytes # the second argument
new 64 bytes # memory for two vector items
new 56 bytes # copy of the 1st argument
new 62 bytes # copy of the 2nd argument
make_vector, two long strings:
new 62 bytes # the second argument
new 56 bytes # the first argument
new 64 bytes # memory for two vector items
Строки больше не копируются при использовании make_vector
, что также удобно.