оператора [ ], поскольку эта функция существует только в map
.
Согласитесь, игра не стоит свеч. Контейнеры действительно
Но рано или поздно наступит день, когда окажется, что первоначальный выбор контейнера был, мягко говоря, не оптимальным, и вы захотите переключиться на другой тип. При изменении типа контейнера нужно не только исправить ошибки, обнаруженные компилятором, но и проанализировать весь код, где он используется, и разобраться, что следует изменить в свете характеристик нового контейнера и правил перехода итераторов, указателей и ссылок в недействительное состояние. Переходя с vector
на другой тип контейнера, вы уже не сможете рассчитывать на С-совместимую структуру памяти, а при обратном переходе нужно проследить за тем, чтобы контейнер не использовался для хранения bool
.
Если вы знаете, что тип контейнера в будущем может измениться, эти изменения можно упростить обычным способом — инкапсуляцией. Одно из простейших решений основано на использовании определений typedef
для типов контейнера и итератора. Следовательно, фрагмент
class Widget{...};
vector<Widget> vw;
Widget bestWidget;
… // Присвоить значение bestWidget
vector<Widget>::iterator i =// Найти Widget с таким же значением,
find(vw.begin(),vw.end().bestWidget) // как у bestWidget
записывается в следующем виде:
class Widget{...};
typedef vector<Widget> WidgetContaner;
typedef WidgetContainer:.iterator WCIterator;
WidgetContaner vw;
Widget bestWidget;
WCIterator i =find(vw.begin().vw.end(),bestWidget):
Подобная запись значительно упрощает изменение типа контейнера, что особенно удобно, когда изменение сводится к простому добавлению нестандартного распределителя памяти (такое изменение не влияет на правила недействительности итераторов/указателей/ссылок).
class Widget{...};
template<typename T>// В совете 10 объясняется, почему
Specia1Anocator{...}; // необходимо использовать шаблон
typedef vector<Widget.Specia1Anocator<Widget» WidgetContainer;
typedef WidgetContainer::iterator WCIterator;
WidgetContainer vw;// Работает
Widget bestWidget;
WCIterator i=find(vw.begin().vw.end().bestWidget); // Работает
Даже если вас не интересуют аспекты typedef
, связанные с инкапсуляцией, вы наверняка оцените экономию времени. Предположим, у вас имеется объект типа
map<string,
vector<Widget>::iterator,
CIStringCompare>// ClStringCompare - сравнение строк
// без учета регистра: см. совет 19
и вы хотите перебрать элементы множества при помощи const_iterator
. Захочется ли вам вводить строку
map<string.vector<Widget>::iterator,CIStringCompare>::const_iterator
больше одного раза? После непродолжительной работы в STL вы поймете, что typedef
— ваш друг.
Typedef
всего лишь определяет синоним для другого типа, поэтому инкапсуляция производится исключительно на лексическом уровне. Она не помешает клиенту сделать то, что он мог сделать ранее (и не позволит сделать то, что было ранее недоступно). Если вы захотите ограничить зависимость клиента от выбранного типа контейнера, вам понадобятся более серьезные средства — классы.
Чтобы ограничить объем кода, требующего модификации при замене типа контейнера, скройте контейнер в классе и ограничьте объем информации, доступной через интерфейс класса. Например, если вам потребуется создать список клиентов, не используйте класс list
напрямую, определите класс CustomerList
и инкапсулируйте list
в его закрытой части:
class CustomerList {
private:
typedef list<Customer> CustomerContainer;
typedef CustomerContainer::iterator CCIterator;
CustomerContainer customers:
public: // Объем информации, доступной
// через этот интерфейс, ограничивается
};
На первый взгляд происходящее выглядит глупо. Ведь список клиентов — это
предполагалось вначале, зато нужно быстро выделить 20% клиентов с максимальным объемом сделок — эта задача просто создана для алгоритма nthelement
(совет 31). Однако nthelement
требует итератора произвольного доступа и не будет работать с контейнером list
. В этой ситуации «список» лучше реализовать на базе vector
или deque
.
Рассматривая подобные изменения, необходимо проанализировать все функции класса CustomerList
, а также всех «друзей» (friend
) и посмотреть, как на них отразится это изменение (в отношении быстродействия, недействительности итераторов/указателей/ссылок и т. д.), но при грамотной инкапсуляции деталей реализации CustomerList
это изменение практически не повлияет на клиентов CustomerList
.
Совет 3. Реализуйте быстрое и корректное копирование объектов в контейнерах
В контейнерах хранятся объекты, но не те, которые вы им передаете. Более того, при получении объекта из контейнера вам предоставляется не тот объект, который находился в контейнере. При включении объекта (вызовом insert, push_back
и т. д.) в контейнер заносится front
или back
) вы также получаете
Но и после того, как объект окажется в контейнере, он может участвовать в операциях копирования. В результате вставки или удаления элементов в vector, string
и deque
существующие элементы контейнера обычно перемещаются (копируются) в памяти (советы 5 и 14). Алгоритмы сортировки (совет 31), next_permutation
и previous_permutation
; remove
, unique
и их родичи (совет 32); rotate
и reverse
— все эти операции приводят к копированию объектов. Да, копирование объектов действительно занимает очень важное место в STL.
Возможно, вам будет интересно узнать, как же производится копирование. Очень просто — объект копируется вызовом соответствующих функций этого объекта, а точнее
class Widget{ public:
Widget(const Widget&):// Копирующий конструктор
Widget& operator=(const Widget&);// Копирующий оператор присваивания
}: ''
Как обычно, если вы не объявите эти функции самостоятельно, компилятор сделает это за вас. Встроенные типы (int
, указатели и т. д.) копируются простым копированием их двоичного представления. Копирующие конструкторы и операторы присваивания описаны в любом учебнике по С++. В частности, эти функции рассмотрены в советах 11 и 27 книги «Effective С++».
Теперь вам должен быть ясен смысл этого совета. Если контейнер содержит объекты, копирование которых сопряжено с большими затратами, простейшее занесение объектов в контейнер может заметно повлиять на скорость работы программы. Чем больше объектов перемещается в контейнере, тем больше памяти и тактов процессора расходуется на копирование. Более того, у некоторых объектов само понятие «копирование» имеет нетрадиционный смысл, и при занесении таких объектов в контейнер неизменно возникают проблемы (пример приведен в совете 8).
В ситуациях с наследованием копирование становится причиной отсечения. Иначе говоря, если создать контейнер объектов базового класса и попытаться вставить в него объекты производного класса, «производность» этих объектов утрачивается при копировании объектов (копирующим конструктором базового класса) в контейнер:
vector<Widget> vw;
class Special Widget:// SpecialWidget наследует от класса
public Widget{...};// Widget (см. ранее)
SpecialWidget sw;// sw копируется в vw как
vw.push_back(sw);// Специализация объекта теряется (отсекается)
Проблема отсечения предполагает, что вставка объекта производного класса в контейнер объектов базового класса обычно приводит к ошибке. А если вы хотите, чтобы полученный объект обладал