6.9. Хранение контейнеров в контейнерах

6.9. Хранение контейнеров в контейнерах

Проблема

Имеется несколько экземпляров стандартного контейнера (list, set и т.п.) и требуется сохранить их в еще одном контейнере.

Решение

Сохраните в главном контейнере указатели на остальные контейнеры. Например, можно использовать map для хранения ключа типа string и указателя на set как значения. Пример 6.12 показывает простой класс журналирования транзакций, который хранит данные как map из пар, состоящих из string и указателей на set.

Пример 6.12. Хранение набора указателей в отображении

#include <iostream>

#include <set>

#include <map>

#include <string>

using namespace std;

typedef set<string> SetStr

typedef map<string, SetStr*> MapStrSetStr;

// Фиктивный класс базы данных

class DBConn {

public:

 void beginTxn() {}

 void endTxn() {}

 void execSql(string& sql) {}

};

class SimpleTxnLog {

public:

 SimpleTxrLog() {}

 ~SimpleTxrLog() {purge();}

 // Добавляем в список выражение SQL

 void addTxn(const string& id

  const string& sql) {

  SetStr* pSet = log_[id]; // Здесь создается запись для

  if (pSet == NULL) {      // данного id, если ее еще нет

   pSet = new SetStr();

   log_[id] = pSet;

  }

  pSet->insert(sol);

 }

 // Применение выражений SQL к базе данных, по одной транзакции

 // за один раз

 void apply() {

  for (MapStrSetStr::iterator p = log_.begin();

   p != log_.end(); ++p) {

   conn_->beginTxn();

   // Помните, что итератор отображения ссылается на объект

   // типа pair<Key,Val>. Указатель на набор хранится в p->second.

   for (SetStr::iterator pSql = p->second->begin();

    pSql != p->second->end(); ++pSql) {

    string s = *pSql;

    conn_->execSql(s);

    cout << "Executing SQL: " << s << endl;

   }

   conn_->endTxn();

   delete p->second;

  }

  log_.clear();

 }

 void purge() {

  for (MapStrSetStr::iterator p = log_.begin();

   p != log_.end(); ++p)

   delete p->second;

  log_.clear();

 }

 //...

private:

 MapStrSetStr log_;

 DBConn* conn_;

};

Обсуждение

Пример 6.12 предлагает ситуацию, где может потребоваться хранение одного контейнера в другом. Представьте, что требуется сохранить набор выражений SQL в виде пакета, выполнить их в будущем все сразу для реляционной базы данных. Именно это делает SimpleTxnLog. Чтобы сделать его еще полезнее, можно добавить в него другие методы, а для обеспечения безопасности — добавить обработку исключений, но целью этого примера является показать, как хранить один тип контейнеров в другом.

Для начала я создаю несколько typedef, облегчающих чтение кода.

typedef std::set<std::string> SetStr;

typedef std::map<std::string, SetStr*> MapStrSetStr;

При использовании шаблонов шаблонов (шаблонов… и т.д.) объявления становятся очень длинными, что затрудняет их чтение, так что облегчите себе жизнь, использовав typedef. Более того, использование typedef облегчает внесение изменений в объявление шаблонов, избавляя от необходимости выполнять поиск и замену во многих местах большого количества исходных файлов.

Класс DBConn — это фиктивный класс, который представляет подключение к реляционной базе данных. Интересно здесь то, как в SimpleTxnLog определяется метод addTxn. В начале этой функции я смотрю, существует ли уже объект набора для переданного id.

SetStr* pSet = log_[id];

log_ — это map (см. рецепт 6.6), так что operator[] выполняет поиск id и смотрит, связаны ли с ним какие-либо данные. Если да, то возвращается объект данных, и pSet не равен NULL. Если нет, он создается, и возвращается указатель, который будет равен NULL. Затем я проверяю, указывает ли на что-то pSet, и определяю, требуется ли создать еще один набор.

if (pSet == NULL) {

 pSet = new SetStr(); // SetStr = std::set<std::string>

 log_[id] = pSet;

}

Так как pSet — это копия объекта данных, хранящихся в map (указатель на набор), а не само значение, то после создания set я должен поместить его обратно в связанный с ним ключ в map. После этого все, что остается сделать, — это добавить элемент в набор и выйти.

pSet->insert(sql);

Выполнив указанные шаги, я в один контейнер (map) добавил указатель на адрес другого контейнера (set). Что я не делал — это добавление объекта set в map. Разница очень существенна. Так как контейнеры обладают семантикой копирования, следующий код приведет к копированию всего набора s в map.

set<string> s;

// Заполнить s данными...

log_[id] = s; // Скопировать s и добавить его копию в log_

Это приведет к огромному числу дополнительных нежелательных копирований. Следовательно, общее правило при использовании контейнеров из контейнеров — это использовать указатели на контейнеры.