А. Александреску - Современное проектирование на C++ (1119444), страница 38
Текст из файла (страница 38)
Программа пересылает объекту класса ьод все ошибки, возникающие при конкретизации классов кеуЬоагд и п(зр1ау, а также при уничтожении их объектов. Если мы попытаемся реализовать эти классы в виде синглтонов Мейерса, программа будет работать неправильно. Например, допустим, что после успешного создания объекта класса кеуЬоагд инициализация объекта класса пззр1ау потерпела неудачу. Конструктор объекта класса п1зр1ау создает объект класса ьод, в нем регистрируется ошибка, и похоже, что на этом приложение должно завершить свою работу. При выходе из программы вступают в действие правила языка: система поддержки выполнения программ разрушает локальные статические объекты в порядке, обратном очередности их создания.
Следовательно, объект класса ьод будет разрушен до объекта класса кеуЬоагд. Если по некоторой причине объект класса кеуЬоагд не будет успешно уничтожен, а попытается передать объекту класса ьод сообщение о возникшей ошибке, функция-член ьод::1пзтапсе невольно вернет ссылку на "оболочку" разрушенного объекта класса ьод. Таким образом, поведение программы станет непредсказуемым. В этом И заключается проблема висячей ссылки (ореад-ге(егепсе ргоЫет). 157 Глава б. Реализация шаблона Я(пд!етоп Порядок создания и разрушения объектов классов кеуЬоагд, сод и о(ар!ау заранее не определен. Необходимо, чтобы классы кеуЬоагд и о)зр1ау подчинялись правилам языка С++ (последним создан — первым уничтожен), а класс Сод должен быть исключением из этих правил.
Независимо от того, когда он был создан, объект класса Сод всегда должен уничтожаться после объектов классов кеуЬоагд и о(зр1ау, чтобы он мог получать сообщения об ошибках, возникающих при их разрушении. Если приложение содержит несколько взаимодействующих синглтонов, автоматически проконтролировать продолжительность их жизни невозможно.
Нормальный синглтон должен по крайней мере расиозиаваиш висячие ссылки. Этого можно достичь, отслеживая процесс разрушения объектов с помощью статической булевской переменной деэтгоуед . Начальное значение этой переменной равно та1зе. Деструктор класса 5(пд1етоп присваивает ей значение тгце. Перед тем как перейти к реализации, подведем итоги. Кроме создания объекта класса 51пд1етоп и возврата ссылКи на нето, фУнкция-член 5)пд)стоп::1пзтапсе должна распознавать висячую ссылку.
Следуя принципу "одна функция — одна задача", реализуем три разные функции-члена; сгеате, фактически создающую объект класса 5)пд1 етоп, опоеадае1егепсе, выполняющую обработку ошибок, и хорошо известную функцию 1пэтапсе, предоставляющую доступ к объекту класса 5(пд)етоп. Из всех этих функций только 1пэтапсе является открытой. Реализуем класс 5(пд1етоп, распознающий висячую ссылку. Во-первых, добавим в класс 51 пд! етоп статическую булевскую переменную-член дезтгоуеб . Она предназначена для индикации висячей ссылки. Затем изменим деструктор класса 5(пд!етоп, чтобы он присваивал переменной ртпьсапсе значение О, а переменной дезтгоуед — значение тгме. Новый класс и функция опоеадяе1егепсе имеют следующий вид.
// 5(пд1етоп.Ь с1азз 5(пд1етоп ( рмЬ!зс: 51пд1етопд 1пзтапсеО ( 11 Ортпэтапсе ) ( // ПРоверка висячей ссылки (~ (дезтгоуед ) ( опоеадяеФегепсеО; е1зе // первый вызов — инициализация сгеатеО; гетцгп ртпэтапсе ргзчате: // Создаем новый объект класса 5(пд!етоп // и присваиваем указатель на него переменной ртпзтапсе зтат(с чо1д сгеатеО ( // задача: инициализировать переменную р1пзтапсе агат(с 5з'пд1етоп СЬетпзтапсе; ртпзтапсе=-дтпе1пзгапсе; ) 158 Часть й.
Компоненты // вызывается, если обнаружена висячая ссылка зсас1с чо(д опцеабкеГегепсеО сбгоп зсб::гипс!же еггог("обнаружена висячая ссылка"); чтгтца! -5тпд1етопО рспзсапсе = О; дезтгоуед = сгое; // данные 5тпд1етоп рспзтапсе ; Ьоо! дезсгоуеб ; заблокированные операторы ...
//51пд1етоп,срр 5тпд1есоп* 5$пд1есоп::рспзсапсе = О; Ьоо1 5!пд1есоп: гбезсгоуед = ~а1зе; Этот код работает! В момент завершения работы приложения вызывается деструктор класса 5)пд1есоп. Он присваивает переменной рспзсапсе значение О. а переменной с!езсгоуед — значение сгибе.
Если какой-нибудь объект попытается получить доступ к уничтоженному синглтону, поток управления достигнет функции опоеас)аетегепсе, которая сгенерирует исключительную ситуацию гипсзве еггог. Это простое и эффек~ивное решение, не требующее больших затрат ресурсов. 6.6. Проблема адресации висячей ссылки (Ц: Феникс Если описанное выше решение применить к проблеме КРЕ (кеуЬоагс!, О1зр)ау, ьод), результат окажется неутешительным. Если деструктору класса О)зр1ау понадобится сообщить об ошибке уничтоженному объекту класса ьод, функция-член сод::спь сапсе сгенерирует исключительную ситуацию.
Если раньше поведение приложения было непредсказуемым, то теперь оно стало неудовлетворительным. Нам нужно, чтобы доступ к объекту класса ьод был постоянным, независимо от того, когда он был создан. В крайнем случае можно было бы создать объект класса сод вновь (после его уничтожения), чтобы получить возможность использовать его для генерации сообщения об ошибках в любое время.
Эта идея положена в основу шаблона проектирования РЬоепзх 5)пд1етоп, Подобно легендарной птице Феникс, восстающей из пепла, такой синглтон может возникнуть вновь после своего уничтожения. В каждый фиксированный момент времени по-прежнему существует лишь один экземпляр класса 5т пд1есоп (двух синглтонов одновременно быть нс может). Тем не менее при обнаружении висячей ссылки объект этого класса можно создавать вновь.
Используя шаблон проектирования РЬоептх 5)пд1есоп, можно легко решить проблему КРЕ: классы кеубоагд и О)зр1ау остаются обычными синглтонами, а класс сод становится фениксом. Феникс можно легко реализовать с помощью статической переменной. При обнаружении висячей ссылки новый объект класса 5!пд1есоп создается под оболочкой старого. (В языке С++ это возможно. Память, выделенная для статических объектов, остается доступной на всем протяжении рабо~ы программы.) Разрушение нового объекта также регистрируется функцией асех)с. Изменять функцию спзсапсе не нужно.
Все модификации касаются только функции опоеагяеФегепсе. 159 Глава 6. Реализация шаблона 8!пд!е!оп с1азз 51пд1етоп ( как и раньше коза к)11яЬоеп1х5зпд)етопО; // добавлено чозд 5зпд1етоп::опоеадяетегепсеО ( // получаем оболочку уничтоженного синглтона сгеасеО; // теперь указатель ртпзсапсе ссылается // на "пепел" синглтона — ячейки памяти, которые // ранее занимал уничтоженный сннглтон. // на этою же месте создается новый сннглтон пеш(ртпзтапсе ) 51пд1етоп; // ставим новый объект в очередь на уничтожение атех1т(к111яйоеп)х5)пд1етоп); // изменяем значение переменной дезтгоуед // поскольку сннглтон восстановлен дезтгоуед = та1зе; ) ко)6 51 п91 ее оп:: к)11 вЬоеп)х5( пд1 е топ О ( // Снова все превращаем в пепел — вызываем деструктор явно.
// переменной ртпзтапсе присваивается значение О, // а переменной дезтгоуед — значение сгибе ртпзтапсе ->-51пд1етопО; ) Оператор пеш, используемый в Функции опоеадяетегепсе, называется оператором размещения (р1асетепг пеш орегазог). Этот оператор не выделяет память, он лишь создает новый объект и размешает его по указанному адресу, в нашем случае — по адресу, указанному в переменной ртпзтапсе . Интересное обсуждение этого оператора можно найти в книге (Меуегз, 199ВЬ). В приведенном выше описании класса 51пд1етоп добавлена новая функция к)11- ЯЬоепзх51пд1етоп.
Зная, что для возрождения феникса в программе используется оператор размсшения пеш, компилятор больше не разрушает его, как обычную статическую переменную. Мы создаем его вручную, поэтому и уничтожать его должны сами, для чего и предназначен вызов Функции атехз т(кз11яЬоепб х55 пд1етоп).
Проанализируем поток событий. При выходе из приложения вызывается деструктор класса 5)пд1етоп. Он устанавливает указатель на нулевой адрес и присваивает переменной дезтгоуед значение сгибе. Предположим теперь, что к объекту класса 5)п91етоп вновь пытается получить доступ некий глобальный объект. Тогда функция тпзтапсе вызовет функцию опоеадяетегепсе, которая реанимирует объект класса 51пд1етоп и ставит в очередь вызов функции к)11вбоеп1х5)пд1етоп.
После этого функция Хпзтапсе успеШно возвращает ссЬпку на ПравиЛьнЫЙ объект класса 5)пд1етоп. Теперь цикл можно повторять снова. Феникс гарантирует, что глобальный объект и другие синглтоны в любое время могут обратиться к его правильному экземпляру. Это делает феникс привлекательным средством для создания надежных и всегда доступных объектов, таких как объект класса сод. Если сделать класс сод фениксом, программа всегда будет работать правильно. 160 Часть й.
Компоненты 6.6.1. Проблемы, связанные с функцией ежек(1 Если сравнить код, приведенный в предыдущем разделе, с кодом из библиотеки Еой', обнаружится одно отличие. В библиотеке вызов функции агехйт окружен директивой препроцессора №! Ией №(б6еб Атбктт рткбр // Ставим новый объект в очередь на уничтожение агехйт(к(11рйоепйхбзпц1етоп); ()еп6(б Если в программе нет директивы №6ейце Атвхтт ятхбо, вновь созданный феникс не будет уничтожен. Это приведет к утечке памяти, которой мы стремимся избежать. Это происходит потому, что в стандарте языка С++ есть досадный пробел. В нем не описано, что случится, если функция регистрируется с помощью функции атехйт во время вызова, который является следствием другой регистрации функции агех! ш Чтобы проиллюстрировать эту проблему, напишем короткую тестовую программу.
№(пс1ц6е ссзт61(Ь> чо(6 вагО ( чо(6 яооо зт6::атея(т(ваг)! (пт яа(пО ( зс6::атея(т(яоо)! Эта маленькая программа регистрирует функцию яоо с помощью функции атехйц Функция атехйт выполняет вызов агех1т(ваг). Этот случай не описан ни в одном из стандартов языков С и С++. Поскольку функция асехйт и разрушение статических переменных тесно связаны друг с другом, при выхоле из приложения мы теряем почву под ногами. Стандарты языков С и С++ внутренне противоречивы. Они угвержлают, что функция ваг будет вызвана до функции яоо, поскольку функция ваг зарегистрирована последней.