Программирование >>  Включение нужных заголовков 

1 ... 5 6 7 [ 8 ] 9 10 11 ... 71


Но и этот вариант требует больших усилий, чем простой вызов assign. Более того, хотя цикл не встречается в программе, он наверняка присутствует внутри вызова сору (см. совет 43). В результате потенциальное снижение быстродействия не исчезает (вскоре мы поговорим об этом). А сейчас я хочу ненадолго отвлечься от темы и заметить, что практически все случаи использования сору, когда приемный интервал задается итератором вставки (inserter, back inserter или front inserter), могут - и должны - заменяться вызовами интервальных функций. Например, вызов сору заменяется интервальной версией i nsert:

vl.insert(vl.encl(),v2.begin()+v2.size()/2.v2.end()):

Команда получается ненамного короче, но она к тому же ясно указывает на суть происходящего: данные вставляются в vl. Вызов сору означает примерно то же, но не столь очевидно. В данном случае важно не то, что элементы копируются, а то, что в vl добавляются новые данные. Функция insert прямо говорит об этом, а сору лишь сбивает с толку. Нет ничего особенно интересного в том факте, что данные где-то копируются, - собственно, вся библиотека STL построена на принципе копирования. Копирование играет настолько важную роль в STL, что ему посвящен совет 3.

Многие программисты STL злоупотребляют функцией сору, поэтому только что данный совет стоит повторить: вызовы сору, в которых результирующий интервал задается итератором вставки, практически всегда следует заменять вызовами интервальных функций.

Вернемся к примеру с assign. Мы уже выяснили две причины, по которым интервальным функциям отдается предпочтение перед их одноэлементными аналогами.

Написание кода с интервальными функциями обычно требует меньших усилий.

Решения с интервальными функциями обычно выглядят более наглядно и логично.

Короче говоря, программы с интервальными функциями удобнее как писать, так и читать. О чем тут еще говорить?

Впрочем, некоторые склонны относить эти аргументы к стилю программирования, а вопросы стиля вызывают у программистов такую же жаркую полемику, как и тема выбора Лучшего В Мире Редактора (хотя о чем тут спорить? Всем известно, что это Emacs). Было бы неплохо иметь более универсальный критерий для сравнения интервальных функций с одноэлементными. Для стандартных последовательных контейнеров такой критерий существует: эффективность. При работе со стандартными последовательными контейнерами применение одноэлементных функций приводит к более частому выделению памяти, более частому копированию объектов и/или выполнению лишних операций по сравнению с реализацией, основанной на интервальных функциях.

Предположим, вы хотите скопировать массив int в начало vector (исходное размещение данных в массиве может объясняться тем, что данные были получены через унаследованный интерфейс с языком С. Проблемы, возникающие при объ-



единении контейнеров STL с интерфейсом С, описаны в совете 16). Решение с интервальной функцией insert контейнера vector выглядит просто и бесхитростно:

int data[numValues]; Предполагается, что numValues

определяется в другом месте

vector<int> v:

v.insert(v.begin().data,data+numValues): Вставить int из data

в начало v

Вероятно, решение с циклическим вызовом insert выглядит примерно так:

vector<int>: :iterator insertLocCv.beginO): forCint i=0;i<numValues:++i) {

insertLoc = v.insert(insertLoc.data[i]);

Обратите внимание на сохранение значения, возвращаемого при вызове i nsert, до следующей итерации. Если бы значение insertLoc не обновлялось после каждой вставки, возникли бы две проблемы. Во-первых, все итерации цикла после первой повели бы себя непредсказуемым образом, поскольку в результате каждого вызова insert значение i nsertLoc становилось бы недействительным. Во-вторых, даже если бы значение insertLoc оставалось действительным, вставка всегда производилась бы в начале вектора (то есть в v.beginO), и в результате содержимое массива было бы скопировано в обратном порядке.

Попробуем последовать совету 43 и заменим цикл вызовом сору:

сору(data.data+numValues.i nserter(v.v.begi n()));

После создания экземпляра шаблона решение с сору практически идентично решению с циклом, поэтому в своем анализе эффективности мы ограничимся вторым вариантом и будем помнить, что все сказанное в равной степени относится к решению с сору. В слзае с циклом вам будет проще понять, чем обусловлены потери эффективности. Да, это именно потери во множественном числе, поскольку решение с одноэлементной версией insert сопряжено с тремя видами затрат, отсутствующими при использовании интервальной версии insert.

Первая потеря обусловлена лишними вызовами функций. Естественно, последовательная вставка numValues элементов требует numValues вызовов insert. При вызове интервальной формы insert достаточно одного вызова функции, тем самым экономится numValues-1 вызов. Возможно, подстановка (inlining) избавит вас от этих затрат... а может, и нет. Уверенным можно быть лишь в одном: при использовании интервальной формы insert эти затраты заведомо отсутствуют.

Подстановка не спасает от второго вида затрат, обусловленных неэффективностью перемещения существующих элементов v на итоговые позиции после вставки. Каждый раз, когда i nsert включает в v новый элемент, все элементы после точки вставки смещаются на одну позицию, освобождая место. Элемент в позиции р перемещается в позициюр-ьУ и т. д. В нашем примере numValues элементов вставляются в начало v. Следовательно, каждый элемент, находившийся в v до вставки, сдвигается в общей сложности на numVal ues позиций. Но при каждом вызове insert элемент сдвигается только на одну позицию, поэтому это потребует numVal ues перемещений. Если до вставки вектор v содержал п элементов, количество переме-



щений будет равно *numVal ues. В нашем примере вектор v содержит числа типа i nt, поэтому перемешение сведется к простому вызову metnmove, но если бы в v хранились пользовательские типы вроде Widget, то каждое перемещение бьшо бы сопряжено с вызовом оператора присваивания или копирующего конструктора данного типа (в большинстве случаев вызывался бы оператор присваивания, но перемещения последнего элемента вектора обеспечивались бы вызовом копирующего конструктора). Таким образом, в общем случае последовательная вставка numVal ues новых объектов в начало vector<Widget> с п элементами требует *numValues вызовов функций: (/z-l)*numValues вызовов оператора присваивания Widget и numValues вызовов копирующего конструктора Widget. Даже если эти вызовы будут подставляемыми, все равно остаются затраты на перемещение элементов numVal ues раз.

С другой стороны. Стандарт требует, чтобы интервальные функции insert перемещали существующие элементы контейнера непосредственно в итоговые позиции, то есть по одному перемещению на элемент. Общие затраты составят п перемещений (numVal ues для копирующего конструктора типа объектов в контейнере, остальное - для оператора присваивания этого типа). По сравнению с одноэлементной версией интервальная версия insert выполняет на /z*(numValues-l) меньше перемещений. Только задумайтесь: при numValues=100 интервальная форма insert выполняет на 99% меньше перемещений, чем эквивалентный код с многократно повторяющимися вызовами одноэлементной формы insert!

Прежде чем переходить к третьей категории затрат, стоит сделать небольшое замечание. То, что написано в предыдущем абзаце - правда, только правда и ничего, кроме правды, но это не вся правда. Интервальная форма insert может переместить элемент в конечную позицию за одну операцию только в том случае, если ей удастся определить расстояние между двумя итераторами без перехода. Это возможно почти всегда, поскольку такой возможностью обладают все прямые итераторы, а они встречаются практически повсеместно. Все итераторы стандартных контейнеров обладают функциональными возможностями прямых итераторов - в том числе и итераторы нестандартных хэшированных контейнеров (совет 25). Указатели, играющие роль итераторов в массивах, тоже обладают этой возможностью. В общем-то, из всех стандартных итераторов она не присуща только итераторам ввода и вывода. Следовательно, все сказанное выше справедливо в том случае, если итераторы, передаваемые интервальной форме insert, не являются итераторами ввода (скажем, istreami terator - см. совет 6). Только в этом слзае интервальной форме insert приходится перемещать элементы на свои итоговые места по одной позиции, вследствие чего преимущества интервальной формы теряются (для итераторов вывода эта проблема вообще не возникает, поскольку итераторы вывода не могут использоваться для определения интервала insert).

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



1 ... 5 6 7 [ 8 ] 9 10 11 ... 71

© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки.
Яндекс.Метрика