Чтобы узнать, используется ли подсчет ссылок в вашей реализации
string
, проще всего обратиться к документации библиотеки. Поскольку подсчет ссылок считается оптимизацией, разработчики обычно отмечают его среди положительных особенностей библиотеки. Также можно обратиться к исходным текстам реализации
string
. Обычно я не рекомендую искать нужную информацию таким способом, но иногда другого выхода просто не остается. Если вы пойдете по этому пути, не забывайте, что
string
является определением типа для
basic_string<char>
(а
wstring
— для
basic_string<wchar_t>
), поэтому искать следует в шаблоне
basic_string
.
Вероятно, проще всего обратиться к копирующему конструктору класса. Посмотрите, увеличивает ли он переменную, которая может оказаться счетчиком ссылок. Если такая переменная будет найдена,
string
использует подсчет ссылок, а если нет — не использует… или вы просто ошиблись при поиске.
Если доступная реализация
string
построена на подсчете ссылок, а ее использование в многопоточной среде порождает проблемы с быстродействием, возможны по крайней мере три разумных варианта, ни один из которых не связан с отказом от STL. Во-первых, проверьте, не позволяет ли реализация библиотеки отключить подсчет ссылок (обычно это делается изменением значения препроцессорной переменной). Конечно, переносимость при этом теряется, но с учетом минимального объема работы данный вариант все же стоит рассмотреть. Во-вторых, найдите или создайте альтернативную реализацию
string
(хотя бы частичную), не использующую подсчета ссылок. В-третьих, посмотрите, нельзя ли использовать
vector<char>
вместо
string
. Реализации
vector
не могут использовать подсчет ссылок, поэтому скрытые проблемы многопоточного быстродействия им не присущи. Конечно, при переходе к
vector<char>
теряются многие удобные функции контейнера
string
, но большая часть их функциональности доступна через алгоритмы STL, поэтому речь идет не столько о сужении возможностей, сколько о смене синтаксиса.
Из всего сказанного можно сделать простой вывод — массивы с динамическим выделением памяти часто требуют лишней работы. Чтобы упростить себе жизнь, используйте
vector
и
string
.
Совет 14. Используйте reserve для предотвращения лишних операций перераспределения памяти
Одной из самых замечательных особенностей контейнеров STL является автоматическое наращивание памяти в соответствии с объемом внесенных данных (при условии, что при этом не превышается максимальный размер контейнера — его можно узнать при помощи функции
max_size
). Для контейнеров
vector
и
string
дополнительная память выделяется аналогом функции
realloc
. Процедура состоит из четырех этапов:
1. Выделение нового блока памяти, размер которого кратен текущей емкости контейнера. В большинстве реализаций
vector
и
string
используется двукратное увеличение, то есть при каждом выделении дополнительной памяти емкость контейнера увеличивается вдвое.
2. Копирование всех элементов из старой памяти контейнера в новую память.
3. Уничтожение объектов в старой памяти.
4. Освобождение старой памяти.
При таком количестве операций не приходится удивляться тому, что динамическое увеличение контейнера порой обходится довольно дорого. Естественно, эту операцию хотелось бы выполнять как можно реже. А если это еще не кажется естественным, вспомните, что при каждом выполнении перечисленных операций все итераторы, указатели и ссылки на содержимое
vector
или
string
становятся недействительными. Таким образом, простая вставка элемента в
vector/string
может потребовать обновления других структур данных, содержащих итераторы, указатели и ссылки расширяемого контейнера.
Функция
reserve
позволяет свести к минимуму количество дополнительных перераспределений памяти и избежать затрат на обновление недействительных итераторов/указателей/ссылок. Но прежде чем объяснять, как это происходит, позвольте напомнить о существовании четырех взаимосвязанных функций, которые иногда путают друг с другом. Из всех стандартных контейнеров перечисленные функции поддерживаются только контейнерами
vector
и
string
.
• Функция
size
возвращает текущее количество элементов в контейнере. Она не сообщает, сколько памяти контейнер выделил для хранящихся в нем элементов.
• Функция
capacity
сообщает, сколько элементов поместится в выделенной памяти. Речь идет об общемколичестве элементов, а не о том, сколько ещеэлементов можно разместить без расширения контейнера. Если вас интересует объем свободной памяти
vector
или
string
, вычтите
size
из
capacity
. Если
size
и
capacity
возвращают одинаковые значения, значит, в контейнере не осталось свободного места, и следующая вставка (
insert
,
push_back
и т. д.) вызовет процедуру перераспределения памяти, описанную выше.
• Функция
resize(size_t n)
изменяет количество элементов, хранящихся в контейнере. После вызова
resize
функция
size
вернет значение
n
. Если
n
меньше текущего размера, лишние элементы в конце контейнера уничтожаются. Если
n
больше текущего размера, в конец контейнера добавляются новые элементы, созданные конструктором по умолчанию. Если
n
больше текущей емкости контейнера, перед созданием новых элементов происходит перераспределение памяти.
не меньше текущего размера. Обычно это приводит к перераспределению памяти вследствие увеличения емкости (если
n
меньше текущей емкости,
vector
игнорирует вызов функции и не делает ничего, а
string
можетуменьшить емкость до большей из величин (
size, n
)), но размер
string
при этом заведомо не изменяется. По собственному опыту знаю, что усечение емкости
string
вызовом
reserve
обычно менее надежно, чем «фокус с перестановкой», описанный в совете 17.
Из краткого описания функций становится ясно, что перераспределение (выделение и освобождение блоков памяти, копирование и уничтожение объектов, обновление недействительных итераторов, указателей и ссылок) происходит каждый раз, когда при вставке нового элемента текущая емкость контейнера оказывается недостаточной. Таким образом, для предотвращения лишних затрат следует установить достаточно большую емкость контейнера функцией
reserve
, причем сделать это нужно как можно раньше — желательно сразу же после конструирования контейнера.
Предположим, вы хотите создать
vector<int>
с числами из интервала 1–1000. Без использования
reserve
это делалось бы примерно так:
vector<int> v;
for (int i=1; i<=1000; ++i) v.push_back(i):
В большинстве реализаций STL при выполнении этого фрагмента произойдет от 2 до 10 расширений контейнера. Кстати, число 10 объясняется очень просто. Вспомните, что при каждом перераспределении емкость