Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++. Библиотека программиста [rus].pdf
Скачиваний:
279
Добавлен:
16.08.2013
Размер:
2.04 Mб
Скачать

138

1. Перед выполнением обновления каждая транзакция блокирует все объекты, которые она собирается изменить. Если транзакции не удается установить необходимые блокировки, она вообще не начинается.

2.Запрос на блокировку выдается не вначале, а во время транзакции по мере выполнения обновлений. Если необходимый объект уже заблокирован другим клиентом, то транзакция либо ожидает его освобождения, либо завершается неудачей.

Вторая стратегия может привести к взаимной блокировке (deadlock): транзакция A блокирует объект X и ожидает возможности блокировки объекта Y, тогда как транзакция B блокирует объект Y и ожидает возможности блокировки объекта X.

В мире баз данных на эту тему написано огромное количество литературы, поэтому нет смысла ее здесь подробно рассматривать. Нас интересуют те идиомы и возможности С++, которые облегчают решение целого класса подобных задач.

Отмена

Многие графические приложения любезно разрешают пользователю отменить последнюю выполненную операцию. На первый взгляд в этом нет ничего сложного, но в действительности не все так просто. В объектно-ориентированном мире найдется не так уж много проблем дизайна, которые вызывают большую головную боль, чем проблемы отмены в сложных приложениях. Чтобы реализовать отмену, многие программисты «зашивают» в программу структуры данных, ориентированные на конкретную операцию, но такой подход чреват ошибками и неустойчив при сопровождении программы.

Внекоторых языках (таких как Lisp) можно буквально делать «снимки» памяти в различные моменты времени. При наличии такого снимка вернуться к прежнему состоянию приложения очень просто. Увы.

ВС++ таких изящных возможностей не предусмотрено; зато наши программы не запрашивают 3 Гб памяти и успевают завершить работу еще до обеда. Так что это замечание стоит воспринимать не как критику С++, а как констатацию факта: проблема есть, и ее необходимо решать.

Вдальнейшем обсуждении будут рассматриваться две вариации на эту тему. Одни приложения всегда предоставляют один уровень отмены, а в других пользователь может выбирать команду Undo снова и снова — программа каждый раз возвращается на одну операцию назад. Конечно, ограничения все же существуют, но обычно не очень жесткие. Это называется «многоуровневой отменой» — проблема, которой мы займемся после одноуровневой отмены. Вторая вариация относится к контекстной отмене. Если пользователь выполняет операцию отмены сначала в одном, а потом — в другом окне, то после возврата к первому окну и выполнения команды Undo обычно следует отменить последнюю операцию только для первого окна, а не для всего приложения. Иначе говоря, операция отмены учитывает контекст приложения на момент своего вызова.

Хватит?

Существуют и другие проблемы, относящиеся к тому же классу, но и сказанного вполне достаточно, чтобы подтолкнуть нас к дальнейшим исследованиям. Большинство программистов рассматривает эти проблемы как нечто изолированное в контексте конкретного приложения. Однако, как вы вскоре убедитесь, некоторые специфические (а кто-то скажет — извращенные) особенности синтаксиса С++ позволяют приблизиться к общим решениям. Помните: мы изучаем С++ и его идиомы, а не структуры данных или принципы проектирования программ. Многие архитектуры и варианты здесь не приводятся, однако это вовсе не говорит против них.

Образы и указатели

Разве можно начать новую главу и не ввести новую разновидность указателей? Состояние объекта можно восстановить двумя способами: сохранить его образ (image) до и после операции или же хранить информацию, достаточную для выполнения операций в обратном направлении. Вариант достаточно прост и универсален, а попытки возврата через выполнение операций в обратном направлении привязаны к конкретному объекту и приложению, поэтому я предпочитаю хранить несколько копий одного объекта. Ключевая концепция, обеспечивающая реализацию этой методики на С++ — указатель образов (image pointer). Такие указатели незаметно для клиента содержат несколько

139

копий указываемого объекта. Существует множество всевозможных комбинаций и вариаций, так что выбор зависит главным образом от вашего настроения.

Простой указатель образов

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

template <class Type>

 

class ImagePtr {

 

private:

 

Type* current;

// Текущий образ, предоставляемый компоненту

Type* undo;

// Предыдущий образ

public:

 

ImagePtr() : undo(NULL), current(new Type) {} ImagePtr(const ImagePtr<Type>& ip)

: current(new Type(*(ip.current))), undo(NULL) {} ~ImagePtr() { delete current; delete undo; } ImagePtr<Type>& operator=(const ImagePtr<Type>& ip)

{

if (this != &ip) { delete current;

current = new Type(*(ip.current));

}

return *this;

}

void Snapshot()

{

delete

undo;

// На случай, если был старый образ

undo =

current;

 

current = new Type(*undo);

}

void Commit()

{

delete undo; undo = NULL;

}

void Rollback()

{

if (undo != NULL) { delete current; current = undo; undo = NULL;

}

}

Type* operator->() const { return current; }

};

Указатель всегда возвращает «текущий» образ как значение оператора ->, однако за кулисами он прячет предыдущую версию указываемого объекта. Функция Shapshot() создает образ с помощью конструктора копий указываемого объекта. Если клиент передумает и захочет отказаться от изменений, он вызывает функцию Rollback(); если изменения его устраивают, он вызывает функцию Commit().

140

Конструктор копий и оператор = поддерживают семантику ведущих указателей, но не создают снимков во время конструирования или сразу же после присваивания. Помните: нас интересует состояние указываемого объекта, а не указателя. Когда мы создаем новую копию объекта для поддержания семантики ведущего указателя, для этой копии еще не существует предыдущих образов, которые необходимо отслеживать.

Деструктор предполагает, что если клиент не вызвал функцию Commit(), что-то было сделано неверно, и уничтожает копию. Специалисты по базам данных — отъявленные пессимисты; если происходит что-то непредвиденное, они всегда предполагают самое худшее. Во многих приложениях при уничтожении указателя вместо Rollback() следовало бы просто вызвать Commit(), предполагая, что до настоящего момента все происходило вполне разумно.

Стеки образов

Для многоуровневой отмены вам может понадобиться стек предыдущих образов. Один из вариантов реализации — хранить стек в каждом указателе образов. В следующем фрагменте предполагается, что у вас имеется параметризованный класс Stack с функциями Empty(), Push(), Pop() и DeleteAll(). Функция Pop() возвращает верхний элемент стека или NULL, если стек пуст. Функция DeleteAll() опустошает стек и уничтожает все объекты по мере их извлечения. Каждый указатель хранит стек предыдущих образов. Если стек пуст, значит, образы не создавались. Если стек не пуст, в его нижней части находится исходный объект. Функция Rollback() находит и восстанавливает этот объект. Конструктор копий и оператор = работают так же, как и в описанном выше простейшем указателе образов.

template <class Type>

 

 

class ImageStackPtr {

 

 

private:

 

 

Type* current;

// Текущий образ, предоставляемый клиенту

Stack<Type> history;

// Предыдущие образы

public:

ImageStackPtr() : current(new Type) {} ImageStackPtr(const ImageStackPtr<Type>& ip)

: current(new Type(*(ip.current))) {} ~ImageStackPtr() { delete current; }

ImageStackPtr<Type>& operator=(const ImageStackPtr<Type>& ip)

{

if (this != &ip) { history.DeleteAll(); delete current;

current = new Type(*(ip.current));

}

return *this;

}

void PushImage()

{

history.Push(current); current = new Type(*current);

}

void Commit() { history.DeleteAll(); }

void PopImage()

// Вернуться на один уровень

{

 

if (!history.Empty()) { delete current;

current = history.Pop();

141

}

}

void Rollback() // Вернуться к самому старому образу

{

Type* old = history.Pop();

Type* oldere = NULL;

 

if (old != NULL) {

// Хотя бы один раз

while ((older = history.Pop()) != NULL) { delete old;

old = older;

}

delete current; current = old;

}

}

Type* operator->() const { return current; }

};

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

Образы автоматических объектов

Концепцию образа можно немного обобщить, чтобы она распространялась не только на объекты, созданные оператором new и обслуживаемые *-указателями, но и автоматические объекты. Автоматическими считаются стековые объекты, а также переменные и компоненты базовых классов вмещающего объекта независимо от того, выделялась память под вмещающий объект динамически или нет.

template <class Type> class AutoImage { private:

Type current; Type image;

bool have_image; // Истина, если образ существует public:

AutoImage() : have_image(false) {} AutoImage(const AutoImage<Type>& ai)

: current(ai.current), image(), have_image(false) {} AutoImage<Type>& operator=(const AutoImage<Type>& ip)

{

if (this != &ip) { current = ip.current; have_image = false;

}

return *this;

}

AutoImage<Type>& operator=(const Type& t)

{

current = t;

142

return *this;

}

operator Type&() { return current; } void Snapshot()

{

image = current; have_image = true;

}

void Commit() { have_image = false; } void Rollback()

{

current = image; have_image = false;

}

bool HaveImage() { return have_image; }

};

Этот шаблон работает со всеми классами, которые удовлетворяют двум условиям:

1. Тип, используемый в качестве параметра, имеет конструктор без аргументов. Он используется в конструкторе AutoImage для инициализации current и image.

2.Тип, используемый в качестве параметра, допускает присваивание с помощью оператора = по умолчанию, предоставленного компилятором, или перегруженного варианта для данного типа. Используется в функциях Shapshot() и Rollback().

Все встроенные типы (такие как int и double) удовлетворяют этим условиям. Подходят и другие классы, имеющие конструктор без аргументов и рабочий оператор =. Чем дольше я имею дело с C++, тем чаще мне кажется, что нарушение этих требований — проявление злостного непрофессионализма, за которое следует наказывать парой лет каторжного программирования на BASIC. Заодно я бы издал закон о том, чтобы конструкторы копий всегда работали так, как им положено.

Конструктор копий AutoImage следует примеру ImagePtr и ImageStackPtr — он использует конструктор без аргументов для создания фиктивного объекта image и присваивает have_image значение false. Оператор = делает то же самое, однако в нем не удается найти удобный способ уничтожить объект переменной image. Мы выбираем меньшее из двух зол — объект остается без изменений и попросту игнорируется, поскольку переменная have_image равна false. Если вас это не устраивает и вы действительно хотите оставить объект image неинициализированным до тех пор, пока в нем не появится настоящий образ, и уничтожить его после присвоения false переменной have_image, имеются два возможных решения:

1. Изменить тип image с Type на Type* и выделять для него память оператором new. Это увеличит накладные расходы по сравнению с автоматическими объектами, однако вы сможете

в полной мере контролировать процесс создания и уничтожения.

2.Воспользоваться идиомой «виртуальных конструкторов» из главы 13. Не вдаваясь в подробности, скажу, что это позволит вам объявить image чем-то приятным для глаза — например, unsigned char image(sizeof Type) — нежели вызывать конструктор и деструктор Type вручную. Компиляторы С++ недолюбливают подобные фокусы, поэтому, прежде чем пускаться на авантюры, внимательно прочитайте главу 13.

Если AutoImage будет использоваться только для структур или классов, добавьте оператор ->:

Type* operator->() { return &current; }

Обратите внимание: в отличие от предыдущих версий -> этот оператор не может быть константной функцией, поскольку current находится внутри *this и мы не можем гарантировать, что -> не будет использоваться для обращений к неконстантным функциям current.

143

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

class Foo { private:

AutoImage<int> some_integer; AutoImage<Bar> bar;

public:

void Rollback()

{

some_integer.Rollback(); bar.Rollback();

}

void Commit()

{

some_integer.Commit(); bar.Commit();

}

void Snapshot()

{

some_integer.Snapshot(); bar.Snapshot();

}

int ProvideInt() const { return some_integer; } void ChanheInt(int new_value)

{

if (!some_integer.HaveImage()) some_integer.Snapshot();

int&(some_integer) = new_value;

}

const Bar& ProvideBar() const { return bar; } Bar& UpdateBar()

{

if (!bar.HaveImage()) bar.Shapshot();

return Bar&(bar);

}

};

Предполагается, что Bar соответствует необходимым условиям. Последние четыре функции перед тем, как обновлять переменную, создают «моментальный снимок» объекта. Для int получение копии по определению является константным по отношению к копируемой переменной. При вызове функции, изменяющей значение переменной, настает время делать снимок. Для работы с другой переменной, bar, предоставляется как константная, так и неконстантная функция. Конечно, хотелось бы просто перегрузить функцию ProvideBar(), чтобы одна перегруженная версия возвращала const Bar&, а другая — неконстантный Bar&, но тогда их сигнатуры будут совпадать. Помните: две функции не могут иметь одинаковые имена и аргументы и отличаться только типом возвращаемого значения. Я никогда не понимал этого ограничения С++, которое запрещает создавать константную и неконстантную версию оператора ->:

const

Type* operator->() const;

//

Снимок не создается

Type*

operator->() const;

//

Создает снимок

Соседние файлы в предмете Программирование на C++