По ходу дела, программист познакомится со множеством полезных классов и техник, существенно повышающих качество кода. Ну а приведенная в конце документация, автоматически получаемая по исходному коду, заставит задуматься о практической полезности правильного его оформления.
Кроме документации, к тексту прилагается и сам исходный код. Надеюсь, что создать по нему Makefile
для вашего любимого компилятора не составит особого труда.
Буду рад всем содержательным комментариям и исправлениям, которыми вы захотите со мной поделиться.
С уважением, Сергей Деревяго.
Хорошие правила трудно выработать. Еще труднее постоянно им следовать, но все та же практика применения показывает, что создание любого, сколь-нибудь сложного кода командой разработчиков просто невозможно без дисциплины, налагающей на каждого из нас "ненужные требования".
Итак, перед вами находится тот минимальный набор правил, обязательное следование которым является вопросом производственной дисциплины, того, что входит в круг наших обязанностей по факту заключения трудового соглашения.
/** */
), текст которого попадет впоследствии в online документацию. Достаточно хорошей практикой является самостоятельное получение документации по собственным файлам для проверки удобочитаемости.
using namespace std
, т.к. ее действие уже невозможно отменить, и включивший ваш код программист получит множество ненужных проблем.
CException
. Внешние исключения, возбуждаемые чужим кодом, должны инкапсулироваться в соответствующие классы, унаследованные от ExternalCException
.
windows.h
или unistd.h
), т.к. это создает труднопреодолимые проблемы при портировании. Все, что можно использовать в прикладном коде -- это определенные в стандарте C++ средства плюс ваши собственные интерфейсы, использующиеся для сокрытия деталей реализации, зависящих от платформы.
sh_ptr
sh_ptr
-- это наиболее важный представитель семейства умных указателей, без которых современное программирование на C++ просто немыслимо:
/** * Данный класс является разделяемым "умным указателем" с подсчетом ссылок на * объект типа T. В его задачу входит автоматическое уничтожение переданного * объекта, когда на него не останется больше ссылок. * Если необходимо "забрать" владение переданным указателем у всех копий sh_ptr, * то нужно воспользоваться вызовом set(0), в результате чего деструктор sh_ptr * ничего не уничтожит, т.к. delete 0 является пустой операцией. */ template <class T> class sh_ptr { /** разделяемое представление */ struct Rep { /** указатель на разделяемый объект */ T* ptr; /** количество ссылок */ size_t refs; /** создает представление с единственной ссылкой */ Rep(T* ptr_) : ptr(ptr_), refs(1) {} /** удаляет разделяемый объект */ ~Rep() { delete ptr; } /** для ускорения работы */ void* operator new(size_t) { return fixed_alloc<Rep>::alloc(); } /** для ускорения работы */ void operator delete(void* ptr, size_t) { fixed_alloc<Rep>::free(ptr); } }; /** указатель на разделяемое представление */ Rep* rep; public: /** * Создает объект, ответственный за уничтожение переданного указателя. * Указатель будет корректно удален даже в случае возникновения исключения * в конструкторе sh_ptr, т.о. вызов sh_ptr<T>(new T) не приведет к утечке * ресурсов. Тем не менее, не стоит забывать, что вызов вроде * f(sh_ptr<T1>(new T1), sh_ptr<T2>(new T2)) уже чреват потерей объектов, * т.к. порядок вычисления подвыражений не определен и новые объекты T1 и * T2 могут быть созданы до соответствующих sh_ptr, отвечающих за * освобождение ресурсов. */ explicit sh_ptr(T* ptr=0) { try { rep=new Rep(ptr); } catch (...) { delete ptr; throw; } } /** * Копирует переданный объект просто увеличивая количество ссылок на * разделяемый указатель. */ sh_ptr(const sh_ptr& shp) { rep=shp.rep; rep->refs++; } /** * Уменьшает количество ссылок на разделяемый указатель и удаляет его, * если на него никто больше не ссылается. */ ~sh_ptr() { if (--rep->refs==0) delete rep; } /** * Присваивает новое значение. Количество ссылок на старый разделяемый * указатель уменьшается (что может привести к его удалению), а на новый * -- увеличивается. */ sh_ptr& operator=(const sh_ptr& shp) { shp.rep->refs++; if (--rep->refs==0) delete rep; rep=shp.rep; return *this; } /** * Обменивает содержимое объектов. */ void swap(sh_ptr& shp) { Rep* tmp=rep; rep=shp.rep; shp.rep=tmp; } /** * Возвращает ссылку на разделяемый объект при применении к sh_ptr * оператора разыменования. */ T& operator*() const { return *rep->ptr; } /** * Возвращает указатель на разделяемый объект при применении к sh_ptr * оператора ->. */ T* operator->() const { return rep->ptr; } /** * Возвращает значение разделяемого указателя. */ T* get() const { return rep->ptr; } /** * Устанавливает значение разделяемого указателя, т.е. все копии текущего * sh_ptr станут указывать на новый объект. */ void set(T* ptr) { rep->ptr=ptr; } /** * Возвращает количество ссылок. */ size_t refs() const { return rep->refs; } };Рассмотрим маленький пример, иллюстрирующий основные возможности
sh_ptr
:
void f() { // создаем себе явную "реализацию строки с подсчетом ссылок", т.е. при // копировании указателя p1 сам объект string("просто строка") копироваться не // будет, что может привести к существенному росту производительности sh_ptr<string> p1(new string("просто строка")); // копируем p1, теперь p1 и p2 указывают на один и тот же объект sh_ptr<string> p2=p1; // естественно, и у p1 и у p2 количество ссылок равно 2 assert(p1.refs()==2); assert(p2.refs()==2); // получаем размер строки, данный синтаксис возможен благодаря перегрузке // operator->() int sz=p1->size(); // присваиваем строке новое значение посредством перегруженного operator*() // естественно, и p1 и p2 теперь будут указывать на новое значение *p2="новое значение"; // а сейчас, p1 будет указывать на старый объект со значением "новое значение", // в то время как p2 -- уже на новый, но с тем же значением p2=sh_ptr<string>(new string("новое значение")); // т.е. будут равны значения объектов assert(*p1==*p2); // но не их адреса assert(p1.get()!=p2.get()); // явно получаем значение указателя для собственных нужд string* ptr=p1.get(); // а вот это уже серьезная ошибка: p3 ничего не знает о том, что значением // указателя ptr владеет p1 (чей деструктор применит к нему delete) и, // следовательно, тоже будет пытаться удалить ptr в деструкторе sh_ptr<string> p3(ptr); // исправляем ошибку: p3 теперь владеет нулевым указателем и его деструктор // никаких разрушительных действий не произведет, т.к. delete 0 суть пустая // операция p3.set(0); // а по выходу из блока деструкторы всех sh_ptr корректно уничтожат // принадлежащие им объекты }Хоть это, в принципе, и очевидно, но еще раз напомню, что не стоит создавать сложные объекты, ведущие внутри себя подсчет ссылок для ускорения копирования по значению. Правильный подход -- это понимание того факта, что пользователь и сам прекрасно может получить необходимую функциональность путем использования указателя
sh_ptr
на ваш объект. И еще, в отличие от уродца-std::auto_ptr
, класс sh_ptr
может и должен применяться со страндартными контейнерами, т.е. использование std::vector<sh_ptr<ClassName> >
-- это довольно удачная идея, гарантирующая автоматическое удаление объектов в деструкторе вектора.
CException
CException
и производные от него классы.
throw
) всегда один и тот же -- это sh_ptr<CException>
.
CException
определен следующим образом:
/** * Общий базовый класс для всех исключений проекта. Все исключения должны * возбуждаться в виде sh_ptr<CException>, который содержит указатель на * собственно исключение, производное от CException. В принципе, отдельный * производный класс исключений стоит создавать только в том случае, когда у * пользователя должна быть возможность отличить его от "просто CException", а * в остальный случаях вполне подойдет и сам CException, означающий: "обнаружена * общего вида ошибка со следующим описанием". */ class CException { public: /** * Структура для инкапсуляции информации о месте возникновения исключения. */ struct FileLine { /** имя файла */ const char* fname; /** номер строки */ int line; /** * Создает объект по переданным имени файла и строке. */ FileLine(const char* f, int l) : fname(f), line(l) {} }; #ifdef DERS_RETHROW_BUG /** указывает на последний созданный и пока не уничтоженный объект */ static sh_ptr<CException> current; #endif private: /** запрещаем копирование */ CException(const CException&); /** запрещаем присваивание */ CException& operator=(const CException&); protected: /** * Защищенный конструктор. Для создания объектов необходимо использовать * функцию newCException(). */ CException(const FileLine& loc, const std::string& msg, sh_ptr<CException> nest); public: /** место возникновения исключения */ const FileLine location; /** сообщение */ const std::string message; /** указатель на вложенное исключение, причину текущего, или 0 */ const sh_ptr<CException> nested; /** * Предназначена для создания объектов вместо конструктора. Создает объект * в свободной памяти по переданным месту исключения, сообщению и, * возможно, вложенному исключению. Для передачи объекта loc удобно * использовать макрос _FLINE_. */ friend sh_ptr<CException> newCException(const FileLine& loc, const std::string& msg, sh_ptr<CException> nest=sh_ptr<CException>()); /** * Виртуальный деструктор. */ virtual ~CException(); /** * Возвращает имя класса-исключения. Должна обязательно переопределяться в * производных классах. */ virtual std::string getClassName() const; /** * Возвращает "полное описание" исключения, включающее имя класса и место * возникновения. Должна переопределяться производными классами, имеющими * дополнительные данные. */ virtual std::string toString() const; /** * Возвращает "полное описание" текущего исключения и всех исключений, * вложенных в него. Фактически, результаты применения toString() к * каждому из исключений объединяются в одну строку. */ std::string toStringAll() const; /** * Удобная функция для проверки является ли текущее исключение исключением * класса E (или производным от него). Возвращает ненулевой указатель * запрашиваемого типа в случае положительного ответа. */ template<class E> E* is() { return dynamic_cast<E*>(this); } /** * Удобная функция для проверки является ли текущее константное исключение * исключением класса E (или производным от него). Возвращает ненулевой * указатель запрашиваемого типа в случае положительного ответа. */ template<class E> const E* is() const { return dynamic_cast<const E*>(this); } /** * Переопределенный для ускорения работы распределитель памяти. Корректно * работает и с производными классами. */ void* operator new(size_t size) { return sized_alloc::alloc(size); } /** * Переопределенный для ускорения работы распределитель памяти. Корректно * работает и с производными классами. */ void operator delete(void* ptr, size_t size) { sized_alloc::free(ptr, size); } };Остановимся подробно на каждом из пунктов.
int
или string
), очевидно, не самая удачная идея. Необходимо использовать иерархию специализированных классов.
std::exception
) также нежелательно в силу того, что они имеют не вполне подходящую нам функциональность и в чужом коде за ними уже закреплен вполне определенный смысл.
sh_ptr<CException>
? Дело в том, что использование sh_ptr
с исключениями имеет целый ряд преимуществ:
std::terminate()
. В то время как при использовании sh_ptr<CException>
объект CException
все время обработки продолжает жить в одном и том же месте выделенной посредством new
памяти, а копируются только объекты sh_ptr
, чей конструктор копирования работает быстро и никаких исключений не возбуждает.
CException
содержат указатель sh_ptr<CException> nested
на исключение, являющееся причиной текущего. Такой подход позволяет сохранить точную информацию о всех возникших исключениях, а также получить некий аналог stack trace, пройдя по цепочке объектов-исключений (в принципе, строка, возвращаемая функцией CException::toStringAll()
, вполне подходит для данной цели).
toCException()
для перехвата исключений можно всегда использовать один-единственный catch (...)
блок, что очень удобно.
ceexample.cpp |
---|
/** @file * Отдельный файл-пример, иллюстрирующий вопросы, связанные с обработкой * исключений. К проекту ftext он не относится. */ #include <stdio.h> #include <vector> #include "cexception.hpp" using namespace std; /** исключение-пример использования CException в качестве базового класса */ class ExampleCException : public CException { protected: ExampleCException(const FileLine& loc, const std::string& msg, sh_ptr<CException> nest) : CException(loc, msg, nest) {} public: friend sh_ptr<CException> newExampleCException(const FileLine& loc, const std::string& msg, sh_ptr<CException> nest=sh_ptr<CException>()); virtual std::string getClassName() const { return "ExampleCException"; } }; inline sh_ptr<CException> newExampleCException(const CException::FileLine& loc, const std::string& msg, sh_ptr<CException> nest) { #ifndef DERS_RETHROW_BUG // обычная версия тела функции return sh_ptr<CException>(new ExampleCException(loc, msg, nest)); #else // версия для компиляторов, некорректно обрабатывающих перевозбуждение // исключений // в этом случае возбуждаемое исключение следует сохранять в переменной // CException::current, чтобы функция toCException() смогла впоследствии // получить (последнее возбужденное) исключение не перевозбуждая его return CException::current=sh_ptr<CException>(new ExampleCException(loc, msg, nest)); #endif } int i; void g() { // использование собственного блока try вокруг тела "сложной" функции является // хорошо зарекомендовавшей себя практикой, а если в спецификации функии // написано, что она возбуждает только CException, то без этого просто не // обойтись try { switch (i) { // возбуждаем наше ExampleCException с помощью функции newExampleCException() // макрос _FLINE_ заменяет громоздкое CException::FileLine(__FILE__, __LINE__) case 1: throw newExampleCException(_FLINE_, "Hello from g()"); // неявно возбуждаем std::out_of_range путем доступа за пределы массива case 2: { vector<int> v; v.at(0); } // явно возбуждаем исключение типа int case 3: throw 3; } } // единственный блок catch, перехватывающий все исключения // т.к. используется формат (...), тип перехваченного исключения теряется, тем // не менее, функция toCException() перевозбуждает исключение, определяет его // тип и возвращает нам в виде sh_ptr<CException> // далее это восстановленное исключение передается в качестве nested новому // исключению, содержащему сообщение о проблеме в функции g() catch (...) { throw newCException(_FLINE_, "Problems in g()", toCException(_FLINE_)); } } void f() { try { g(); } // аналогично, перехватываем все исключения и сообщаем о проблеме в функции f() catch (...) { throw newCException(_FLINE_, "Problems in f()", toCException(_FLINE_)); } } int main() { for (i=1; i<=4; i++) { try { if (i<4) f(); // возбуждаем "неизвестное исключение" прямо из этого блока, чтобы показать // зачем функция toCException() также принимает параметр CException::FileLine else throw 4; } // перехватываем возникшие исключения и используем toStringAll() для вывода // полной информации о всей цепочке исключений catch (...) { printf("\tException #%d:\n%s", i, toCException(_FLINE_)->toStringAll().c_str()); } } } |
После запуска на исполнение он выводит следующую информацию:
Exception #1: CException [ceexample.cpp:90]: Problems in f() CException [ceexample.cpp:81]: Problems in g() ExampleCException [ceexample.cpp:64]: Hello from g() Exception #2: CException [ceexample.cpp:90]: Problems in f() CException [ceexample.cpp:81]: Problems in g() STDExternalCException [ceexample.cpp:81], typeName="St12out_of_range", message="vector" Exception #3: CException [ceexample.cpp:90]: Problems in f() CException [ceexample.cpp:81]: Problems in g() UnknownExternalCException [ceexample.cpp:81], typeName="unknown", message="Unknown exception" Exception #4: UnknownExternalCException [ceexample.cpp:107], typeName="unknown", message="Unknown exception"Как можно видеть, цепочки исключений позволяют получить достаточно подробную информацию о происшедшем, а т.к. реальный код вместо загадочного сообщения вроде
"Problems in f()"
предоставляет гораздо более содержательную диагностику, то отслеживание причин ошибочной ситуации еще более упрощается.
Хорошо, с тем, как перехватывать сразу все исключения мы уже разобрались. Но что делать в случае необходимости реализации дифференцированного подхода? Ведь пока не появился sh_ptr
можно было использовать несколько блоков catch
:
void f() { try { g(); } catch (const ExampleCException&) {} // игнорируем ExampleCException catch (...) { throw; } // а все остальные перевозбуждаем }Нет проблем. Нам никто не запрещает использовать несколько блоков
catch
и сейчас, просто они становятся гораздо менее полезны. А предыдущий пример может быть переписан следующим образом:
void f() { try { g(); } catch (...) { sh_ptr<CException> ce=toCException(_FLINE_); if (ce->is<ExampleCException>()) {} // игнорируем ExampleCException else { throw; } // а все остальные перевозбуждаем } }Обратите внимание, что функция
toCException()
не превращает текущее исключение (например, просто int
) в sh_ptr<CException>
-- она просто возвращает объект sh_ptr<CException>
, содержащий информацию о текущем, все еще не обработанном исключении (т.е. else { throw; }
перевозбудит все тот же int
).
Ну а сейчас самое время остановиться и тщательно все обдумать. Изложенный выше способ возбуждения и обработки исключений содержит гораздо больше тонкостей, чем это кажется на первый взгляд. Да и на второй, впрочем, тоже...
Рассмотрим следующий пример. Допустим, что по ходу реализации проекта у нас возникает необходимость сопоставления имен файлов с некоторой маской. В данном случае:
*.[ch]pp
?", ответ на который для пользователей Windows будет далеко не очевиден.
/** * Данный интерфейс предназначен для сопоставления имен файлов с маской. * Предполагается, что маска задается в момент создания объекта, что позволяет * произвести всю необходимую инициализацию только один раз. */ class NameMatcher { public: /** * Выполняет сопоставление переданного имени файла с маской, заданной в * момент создания объекта. Возвращает true, если имя файла удовлетворяет * маске. Возбуждает CException при обнаружении ошибок. */ virtual bool match(const std::string& fileName)=0; /** * Обязательный виртуальный деструктор. */ virtual ~NameMatcher() {} private: /** запрещаем присваивание */ NameMatcher& operator=(const NameMatcher&); };Начиная с этого момента, у нас возникает прекрасная возможность распараллелить ход работ, назначив задание по созданию промышленной реализации
NameMatcher
второму разработчику, в то время как первый продолжит свою работу над прикладным кодом, создав для себя тривиальнейшую тестовую реализацию вроде:
/** временная тестовая реализация */ class NameMatcherTest : public NameMatcher { /** все файлы подходят */ virtual bool match(const std::string&) { return true; } };Думаю, самое время предостеречь начинающих, что создание тестовой реализации
NameMatcherTest
отнють не означает, что прикладной код будет напрямую создавать объекты данного класса
void f() { NameMatcherTest nm; for (;;) { string name; // ... if (nm.match(name)) { // ... } // ... } }а потом всюду заменит имя
NameMatcherTest
на что-то вроде NameMatcherProfessional
, созданное вторым разработчиком. Это означает, что прикладной код будет работать только через интерфейс
void f(sh_ptr<NameMatcher> nm) { for (;;) { string name; // ... if (nm->match(name)) { // ... } // ... } }и никаких массовых замен щедро рассыпанного по коду упоминания
NameMatcherTest
никогда не понадобится: для этого и нужно промежуточное звено -- абстрактный базовый класс NameMatcher
.
"Ну да, все просто замечательно!" -- воскликнет проницательный читатель, -- "Уж не хотите ли вы сказать, что в коде вообще не будет упоминания NameMatcherTest
?" Нет, дорогой читатель! Не хочу и не скажу, т.к. одно упоминание NameMatcherTest
нам наверняка понадобится, но не более того. И речь об этом пойдет в следующем разделе.
#include
чисто системных файлов вроде unistd.h
или windows.h
! Малейшее их упоминание в прикладном коде -- и перенос приложения на другую платформу, или даже на другой компилятор для той же самой платформы, превратится в сущий кошмар! Именно тот кошмар, который и происходит в 95, а то и более, процентах компаний по разработке программного обеспечения. Стандарты нужно уважать! Они, как и наставления по технике безопасности, кровью писаны, и множество судеб уже было и, что более печально, еще будет искалечено путем игнорирования "досадных инструкций", которые "никому не нужны".
Ну ладно, хватит крови, поговорим о более приятных вещах. Прикладной код -- это та основа приложения (процентов 80% всех усилий), которая реализует собственно идею приложения, тем самым отличая его от других. Если пристально вглядеться, то этот код работает со множеством различных источников и стоков данных платформно-независимым способом, и этой его независимостью обязательно нужно воспользоваться. Даже если вы не планируете портирование приложения (ха!) на другие платформы, четкое разделение того, что делается, от того как это делается будет чрезвычайно полезно для проекта.
Хорошо, а что же такое не прикладной код? Это код, отвечающий за "как делается" и, вообще говоря, зависящий от платформы. Именно он знает имена классов, реализующих наши интерфейсы.
Итак, прикладной код работает через ссылки и (умные) указатели на интерфейсы (надеюсь вы помните, что по счастливой случайности в C++ нельзя создавать объекты абстрактных базовых классов?), а зависящий код предоставляет ему объекты, их реализующие. И создаются эти объекты в фабриках объектов, а прикладной код получает указатели на них, и с этих самых пор отвечает за их корректное удаление. К счастью, ответственность за удаление практически всегда можно поручить умным указателям (с подсчетом ссылок).
Определим, например, пространство имен Factory
и разместим в нем функции для создания необходимых нам объектов. Можно, конечно, использовать и класс Factory
с набором статических функций, но пространство имен имеет то неоспоримое преимущество, что может объявляться по частям, т.е. объявление
/** * Фабрика объектов. */ namespace Factory { /** * Создает объект по переданной маске, реализующий интерфейс NameMatcher. В * случае возникновения ошибок возбуждает CException. */ sh_ptr<NameMatcher> newNameMatcher(const std::string& mask); }будет находиться в том же заголовочном файле, что и интерфейс
NameMatcher
, а определение -- вместе с реализацией интерфейса:
namematcherimpl.cpp |
---|
/** @file * Реализация интерфейса NameMatcher. */ #include "namematcher.hpp" #include <ctype.h> using namespace std; namespace { /** * Класс-реализация интерфейса NameMatcher, выполняющая простейшее * сопоставление маски с концом файла. */ class NameMatcherImpl : public NameMatcher { /** суффикс, на который должно заканчиваться имя файла */ string suff; /** индекс последнего символа суффикса */ int send; /** * Проверяет соответствие имени файла маске. Данная простейшая реализация * допускает только маски вида "*ext" (т.е. имя файла должно * заканчиваться на "ext") и игнорирует регистр букв. Звездочка "*" в * начале маски не обязательна, т.е. маска "ext" эквивалентна "*ext". */ virtual bool match(const std::string& fileName); public: /** * Создает объект по переданной маске. */ NameMatcherImpl(const std::string& mask); }; NameMatcherImpl::NameMatcherImpl(const std::string& mask) { int beg= (mask[0]=='*') ? 1 : 0; suff.reserve(mask.size()-beg); for (int i=beg; i<mask.size(); i++) suff.push_back(tolower(mask[i])); send=suff.size()-1; } bool NameMatcherImpl::match(const std::string& fileName) { if (fileName.size()<suff.size()) return 0; for (int fend=fileName.size()-1, i=0; i<suff.size(); i++) if (tolower(fileName[fend-i])!=suff[send-i]) return 0; return 1; } } sh_ptr<NameMatcher> Factory::newNameMatcher(const std::string& mask) { try { return sh_ptr<NameMatcher>(new NameMatcherImpl(mask)); } catch (...) { throw newCException(_FLINE_, "Can't create NameMatcherImpl object", toCException(_FLINE_)); } } |
Такой подход весьма удобен, т.к. определения всех фабрик объектов, с одной стороны, находятся в одном конкретном месте (пространстве имен Factory
), а с другой -- пользователь имеет возможность включить в свой файл объявление только одной конкретной фабрики, которое не потянет за собой объявления всех остальных фабрик проекта со всеми необходимыми им типами. Ну а реализации фабрик объектов разнесены по файлам-реализациям конкретных интерфейсов, что позволяет нам выбирать соответствующий файл на стадии линковки. Он, в частности, может находиться в том самом подкаталоге, зависящем от (имени) платформы, что чрезвычайно упрощает процесс сборки.
Обратите внимание на использование умного указателя sh_ptr
-- важную деталь дизайна фабрики объектов. Фабрика создает объект по требованию пользователя, но не знает, когда необходимо его уничтожить. В то время как пользователь получает указатель на созданный объект и должен (очевидно) не забыть его корректно уничтожить даже в случае возникновения исключений (что тоже должно быть очевидно, но зачастую ускользает от внимания "рядовых разработчиков").
В данном разделе будет описана модель передачи сообщений вида Publisher/Subscriber, т.е. Издатель/Подписчик. Ее отличительной чертой является пересылка сообщения сразу нескольким подписчикам (возможно, ни одному). Издатель публикует сообщения о соответствующих изменениях своего состояния или наступлении определенных событий, а подписчики получают возможность внести изменения в собственное состояние.
Рассмотрим следующий пример. Пусть у нас есть визуальный компонент СтрокаВвода
, публикующий сообщения НажатаКлавиша
. По ходу работы на его сообщения могут независимо друг от друга подписываться объекты пользователя ПроверкаФормата
и ЗаписьМакроса
. Преимущества использования рассылки сообщений очевидны:
СтрокаВвода
ничего не должен знать о существовании объектов ПроверкаФормата
и ЗаписьМакроса
. А о дополнительном объекте АвтодополнениеВвода
, который будет написан через несколько месяцев -- тем более.
ПроверкаФормата
и ЗаписьМакроса
, в свою очередь, ничего не должны знать о существовании компонента СтрокаВвода
, они просто умеют принимать сообщения НажатаКлавиша
. Т.е. ПроверкаФормата
сможет работать и с компонентом ОбластьВводаСКолокольчиками
, если он умеет публиковать сообщения того же класса.
Publisher
и Subscriber
/** * Данный класс является издателем, передающим сообщения типа M всем подписанным * подписчикам (класс Subscriber). Кроме сообщений, передаваемых из функции * send(), Publisher самостоятельно передает сообщения о подписке и прекращении * подписки. Им гарантируется, что подписавшийся подписчик всегда получит * subscribedMsg() и unsubscribedMsg() сообщения (которые могут служить своего * рода аналогами конструктора и деструктора), а все сообщения, передаваемые * посредством send(), будут заключены между ними. Деструктор класса Publisher * корректно информирует всех подписавшихся о прекращении подписки. */ template<class M> class Publisher { friend class Subscriber<M>; /** часть реализации, не зависящая от параметра M */ PubSub_private::PubImpl impl; /** запрещаем копирование */ Publisher(const Publisher&); /** запрещаем присваивание */ Publisher& operator=(const Publisher&); public: /** * Создает объект. */ Publisher() {} /** * Проходит по всем подписчикам и удаляет себя из их списков издателей. В * процессе удаления подписчики получают сообщение о прекращении подписки, * т.е. вызывается их функция unsubscribedMsg(). */ virtual ~Publisher(); /** * Подписывает переданного подписчика (если он еще не был подписан) и * передает ему сообщение о подписке, т.е. вызывается его функция * subscribedMsg(). Возвращает false, если он уже был подписан. */ bool subscribe(Subscriber<M>& sub); /** * Отменяет подписку переданного подписчика (если он был подписан) и * передает ему сообщение о прекращении подписки, т.е. вызывается его * функция unsubscribedMsg(). Возвращает false, если подписчик не был * подписан. */ bool unsubscribe(Subscriber<M>& sub); /** * Возвращает true, если переданный подписчик в настоящее время подписан. */ bool isSubscribed(Subscriber<M>& sub) const { return impl.isSubscribed(&sub); } /** * Возвращает количество подписанных подписчиков. */ int subCount() const { return impl.subCount(); } /** * Отправляет переданное сообщение всем подписанным подписчикам, т.е. * вызывается функция regularMsg() каждого из них. Если regularMsg() * вернет false, то подписчик будет сразу же отписан -- именно поэтому * send() не является const функцией. Возвращает количество подписчиков, * получивших сообщение. */ int send(const M& msg); }; /** * Данный класс является подписчиком сообщений типа M, передаваемых издателем * (класс Publisher). Пользователь должен создать производный класс и * переопределить виртуальные функции, соответствующие тем сообщениям, которые * он хочет получать. Класс Subscriber автоматически ведет список всех * издателей, на сообщения которых он подписан, для корректной отписки в * деструкторе. */ template<class M> class Subscriber { friend class Publisher<M>; /** определение типа для краткости */ typedef typename std::list<Publisher<M>*> list_type; /** список издателей, на сообщения которых подписан данный подписчик */ list_type pubs; /** запрещаем копирование */ Subscriber(const Subscriber&); /** запрещаем присваивание */ Subscriber& operator=(const Subscriber&); public: /** * Создает объект. */ Subscriber() {} /** * Проходит по списку издателей и отписывается от каждого из них. * Переопределенная пользователем Функция unsubscribedMsg() при этом не * вызывается. */ virtual ~Subscriber(); /** * Данная функция может быть переопределена пользователем для обработки * сообщений издателя, посылаемых им из send(). Если она вернет false, то * подписчик будет сразу же отписан (с вызовом unsubscribedMsg()). * Объект-издатель передается по константной ссылке pub для предотвращения * его изменения в процессе передачи сообщения подписчикам, т.к. это может * привести к ошибкам. Все исключения, возбуженные regularMsg(), будут * проигнорированы. */ virtual bool regularMsg(const Publisher<M>& pub, const M& msg); /** * Данная функция может быть переопределена пользователем для получения * сообщений о подписке. Объект-издатель передается по константной ссылке * pub для предотвращения его изменения в процессе передачи сообщения * подписчикам, т.к. это может привести к ошибкам. Все исключения, * возбуженные subscribedMsg(), будут проигнорированы. */ virtual void subscribedMsg(const Publisher<M>& pub); /** * Данная функция может быть переопределена пользователем для получения * сообщений о прекращении подписки. Объект-издатель передается по * константной ссылке pub для предотвращения его изменения в процессе * передачи сообщения подписчикам, т.к. это может привести к ошибкам. Все * исключения, возбуженные unsubscribedMsg(), будут проигнорированы. */ virtual void unsubscribedMsg(const Publisher<M>& pub); };Нетривиальной их частью является необходимость следить за временем жизни издателей и подписчиков, связанных между собой. Для иллюстрации эффектов, возникающих при безвременной кончине издателей/подписчиков, рассмотрим следующий пример:
psexample.cpp |
---|
/** @file * Отдельный файл-пример, иллюстрирующий вопросы, связанные с временем жизни * классов Publisher и Subscriber. К проекту ftext он не относится. */ #include <stdio.h> #include <string> #include "pubsub.hpp" using namespace std; /** для вывода информации о создании/уничтожении объекта */ struct Named { string name; Named(const string& n) : name(n) { printf("constructed: %s\n", name.c_str()); } ~Named() { printf("destructed: %s\n", name.c_str()); } }; /** издатель */ struct Pub : Named, Publisher<string> { Pub(const string& name) : Named(name) {} }; /** подписчик */ struct Sub : Named, Subscriber<string> { Sub(const string& name) : Named(name) {} virtual bool regularMsg(const Publisher<string>& pub, const string& msg) { printf("%s received %s from %s\n", name.c_str(), msg.c_str(), static_cast<const Pub&>(pub).name.c_str()); return 1; } virtual void subscribedMsg(const Publisher<string>& pub) { printf("%s received subscribedMsg from %s\n", name.c_str(), static_cast<const Pub&>(pub).name.c_str()); } virtual void unsubscribedMsg(const Publisher<string>& pub) { printf("%s received unsubscribedMsg from %s\n", name.c_str(), static_cast<const Pub&>(pub).name.c_str()); } }; int main() { Pub p1("p1"); { Sub s1("s1"); { Pub p2("p2"); { Sub s2("s2"); p1.subscribe(s1); p1.subscribe(s2); p2.subscribe(s1); p2.subscribe(s2); p1.send("message1"); p2.send("message1"); } p1.send("message2"); p2.send("message2"); } p1.send("message3"); } p1.send("message4"); } |
Результатом его работы является следующий вывод:
constructed: p1 constructed: s1 constructed: p2 constructed: s2 s1 received subscribedMsg from p1 s2 received subscribedMsg from p1 s1 received subscribedMsg from p2 s2 received subscribedMsg from p2 s1 received message1 from p1 s2 received message1 from p1 s1 received message1 from p2 s2 received message1 from p2 destructed: s2 s1 received message2 from p1 s1 received message2 from p2 s1 received unsubscribedMsg from p2 destructed: p2 s1 received message3 from p1 destructed: s1 destructed: p1Как можно видеть:
s2
издатели не передают ему сообщение message2
, т.е. никаких подвисших ссылок на удаленный объект не возникает. В этом случае, он также не получает и сообщение unsubscribedMsg
.
p2
подписчик s1
получает сообщение unsubscribedMsg
. Если подписчик знает, что никто не мог его отписать с помощью вызова функции unsubscribe()
соответствующего издателя, то получение unsubscribedMsg
свидетельствует о том, что издатель просто прекратил свое существование.
message4
все подписчики уже прекратили свое существование, поэтому данное сообщение не получает никто.
Connection
(соединение с БД) может порождать объекты класса Statement
(SQL-выражение). При этом, мы должны гарантировать, что попытка использования Statement
после того, как породивший его Connection
уже прекратил свое существование будет корректно обработана. И здесь нам на помощь приходит сладкая парочка Publisher
/Subscriber
, которая используется только из-за сообщений unsubscribedMsg
, автоматически генерируемых деструктором издателя:
/** соединение с БД */ class Connection { /** * Издатель, используемый для автоматического информирования объектов * Statement о закрытии соединения. */ Publisher<int> pub; public: /** создает объект Statement по текущему соединению */ sh_ptr<Statement> newStatement() { try { sh_ptr<Statement> ret(new Statement(this)); // создаем объект pub.subscribe(*ret); // и сразу же его подписываем return ret; } catch (...) { throw newDBException(_FLINE_, "Can't create Statement object", toCException(_FLINE_)); } } }; /** SQL-выражение, порожденное по Connection */ class Statement : private Subscriber<int> { friend class Connection; /** * Указатель на соединение, породившее данный объект. Если объект * Connection прекратит свое существование раньше Statement, то указатель * будет обнулен, поэтому все функции его использующие должны обязательно * вызывать checkConn() для проверки. */ Connection* pConn; /** * Данная функция должна вызываться для проверки того, что породившее * Statement соединение все еще существует. В случае обнаружения * несуществующего соединения будет возбуждено DBException. */ void checkConn() { if (!pConn) throw newDBException(_FLINE_, "Connection closed"); } /** * Функция-обработчик сообщения от породившего объекта Connection. Она * будет автоматически вызвана в процессе уничтожения соединения и * обнулит указатель pConn. */ virtual void unsubscribedMsg(const Publisher<int>&) { pConn=0; } /** создает новый объект по соединению */ Statement(Connection* pc) : pConn(pc) { // ... } public: /** исполняет SQL-выражение в рамках породившего соединения */ int execDirect() { checkConn(); // проверяем соединение // ... } };Естественно, это не более чем грубый набросок для иллюстрации применения описанной выше техники. В реальном коде и
Connection
и Statement
являются интерфейсами и живут в соответствующем пространстве имен.
Итак, разрабатываемое нами приложение (упрощенный аналог утилиты grep
) должно:
FindText
В идеале, интерфейс пользователя (ИП) и код ядра приложения должны полностью избежать ненужного зацепления, когда код ИП и код ядра прямо ссылаются на фрагменты друг друга. К счастью, использование интерфейса, четко разделяющего эти две главные части приложения, позволяет нам без проблем достигнуть данного идеала. Думаю понятно, что именно ядро приложения должно скрываться от ИП за интерфейсом, т.к. оно является пассивной стороной, реагирующей на действия пользователя. А по сему, определим интерфейс FindText
, предоставляющий функцию search()
:
class FindText { public: virtual void search(const std::string& dir, bool recur, const std::string& mask, const std::string& text)=0; // ... };Ну вот: ИП с помощью фабрики объектов создает себе объект, реализующий интерфейс
FindText
, вызывает функцию search()
и... Что "и"?! Дело ясное, что реализация search()
прекрасно выполнит работу по поиску текста, заблокировав по ходу дела ИП на заметное (т.е. более 0.1 секунды) время, но где же результат работы? А еще неплохо бы информировать пользователя о достигнутом прогрессе. Да дать ему возможность прервать обработку...
Короче, как бы это реализации search()
сообщать вызвавшей стороне о ходе работ? Не удивлюсь, если вы скажете: "Посредством сообщений!" Правильное решение! Осталось только утрясти номенклатурку.
Классы Publisher
и Subscriber
позволяют нам передавать сообщения некоторого фиксированного типа M
(являющегося их параметром шаблона) или производного от него. Нетрудно видеть, что существует два полярных подхода к определению типов сообщений:
FindText
. Это позволит нам использовать единственную специализацию шаблонов Publisher
/Subscriber
. С другой стороны, пользователя крайне редко интересуют все возможные сообщения сразу, и на их пересылку в издателе и фильтрацию в подписчике будут впустую расходоваться ресурсы.
Publisher
/Subscriber
.
search()
). Также как и сообщения о результатах, они поступают неравномерно, если и поступают вообще.
MsgBase
инкапсулирует общую информацию, а не служит для приведения всех сообщений к одному базовому классу в смысле единой специализации шаблонов Publisher
/Subscriber
):
/** * Данная структура инкапсулирует ссылку на пославший сообщение интерфейс и * является общим базовым классом всех сообщений. Ссылка может быть использована * для прекращения поиска (т.е. для прерывания работы search()) посредством * вызова функции stopSearch(). */ struct MsgBase { /** ссылка на пославший сообщение интерфейс */ FindText& ft; /** * Создает объект по переданным данным. */ MsgBase(FindText& ft_) : ft(ft_) {} }; /** * Данная структура представляет собой сообщение, которое передает интерфейс * FindText своим подписчикам в случае обнаружения искомой строки файла. */ struct FoundMsg : MsgBase { /** имя файла */ const std::string fileName; /** номер строки (с 1) */ const int lineNum; /** текст строки */ const std::string lineText; /** * Создает объект по переданным данным. */ FoundMsg(FindText& ft, const std::string& file, int num, const std::string& txt) : MsgBase(ft), fileName(file), lineNum(num), lineText(txt) {} }; /** * Данная структура представляет собой информационное сообщение о ходе работы, * передаваемое интерфейсом FindText своим подписчикам. */ struct InfoMsg : MsgBase { /** типы информационных сообщений: файл открыт, файл закрыт */ enum Type { opened, closed }; /** тип сообщения */ const Type type; /** имя файла */ const std::string fileName; /** * Создает объект по переданным данным. */ InfoMsg(FindText& ft, Type t, const std::string& fn) : MsgBase(ft), type(t), fileName(fn) {} }; /** * Данная структура представляет собой сообщение о нефатальной ошибке (например, * недостаточно прав доступа для открытия файла) не останавливаюшей обработку, * которое передает интерфейс FindText своим подписчикам. */ struct ErrorMsg : MsgBase { /** сообщение об ошибке */ const std::string errMsg; /** * Создает объект по переданным данным. */ ErrorMsg(FindText& ft, const std::string& err) : MsgBase(ft), errMsg(err) {} };Ну а пополненный интерфейс
FindText
выглядит следующим образом:
/** * Данный интерфейс предназначен для выполнения поиска подстроки в файлах по * маске. Реализующие его классы осуществляют поиск и публикуют сообщения, * информирующие пользователя о ходе работы и позволяющие корректным образом ее * прервать. */ class FindText { public: /** * Выполняет поиск подстроки text в файлах, удовлетворяющих маске mask. * Поиск производится в директории dir и, если задан флаг recur, во всех * ее поддиректориях. В процессе работы происходит рассылка сообщений, на * которые можно подписаться с помощью соответствующих издателей. * Возбуждает CException в случае обнаружения фатальных ошибок. */ virtual void search(const std::string& dir, bool recur, const std::string& mask, const std::string& text)=0; /** * Данная функция предназначена для остановки работы функции search(). Она * может быть вызвана только в процессе обработки сообщения. При * обнаружении ошибок возбуждает CException. */ virtual void stopSearch()=0; /** * Издатель, публикующий сообщения о найденных строках. В процессе * обработки сообщения подписчик может вызвать функцию stopSearch() для * прекращения поиска. */ virtual Publisher<FoundMsg>& foundPub()=0; /** * Издатель, публикующий информационные сообщения о ходе работы. В * процессе обработки сообщения подписчик может вызвать функцию * stopSearch() для прекращения поиска. */ virtual Publisher<InfoMsg>& infoPub()=0; /** * Издатель, публикующий сообщения о нефатальных ошибках, встреченных по * ходу работы. В процессе обработки сообщения подписчик может вызвать * функцию stopSearch() для прекращения поиска. */ virtual Publisher<ErrorMsg>& errorPub()=0; /** * Обязательный виртуальный деструктор. */ virtual ~FindText() {} private: /** запрещаем присваивание */ FindText& operator=(const FindText&); };
FindText
FindText
благополучно определен -- самое время перейти к его реализации. Еще раз напомню, что начиная с этого момента работа по разработке приложения может вестись уже двумя группами (группа ИП и группа ядра) полностью параллельно, т.к. все, что их соединяет -- это интерфейс FindText
, и он уже определен.
Ну а начнем мы, естественно, с определения фабрики объектов, позволяющей пользователям создавать реализации интересующих их интерфейсов:
/** * Фабрика объектов. */ namespace Factory { /** * Создает объект, реализующий интерфейс FindText. В случае возникновения * ошибок возбуждает CException. */ sh_ptr<FindText> newFindText(); }А продолжим уже непосредственно реализацией. Прежде всего отметим, что первое действие реализации функции
search()
-- это (рекурсивный) поиск фалов по маске. К сожалению, стандарт C++ не предоставляет нам средств для осуществления подобного поиска, но, к счастью, мы прекрасно понимаем, что проблема может быть решена путем введения еще одного интерфейса:
findfile.hpp |
---|
/** @file * Определение интерфейса FindFile. */ #ifndef __FINDFILE_HPP__ #define __FINDFILE_HPP__ #include "cexception.hpp" /** * Данный интерфейс предоставляет callback функцию для FindFile. */ class FindFileCallback { public: /** * Данная функция будет вызываться в процессе работы FindFile::find() для * каждого найденного файла. Если она вернет false, то FindFile::find() * завершит свою работу. Исключения, возбужденные found() приведут к * аварийному завершению работы FindFile::find(), которая в этом случае * также возбудит исключение. */ virtual bool found(const std::string& fileName)=0; /** * Обязательный виртуальный деструктор. */ virtual ~FindFileCallback() {} private: /** запрещаем присваивание */ FindFileCallback& operator=(const FindFileCallback&); }; /** * Данный интерфейс предназначен для поиска файлов по маске с возможностью * рекурсивного поиска в поддиректориях. */ class FindFile { public: /** * Выполняет поиск файлов по маске mask в директории dir и, если задан * флаг recur, во всех ее поддиректориях. Для каждого найденного файла * вызывается функция found() переданного объекта FindFileCallback. Если * она вернет false, то поиск прекратится и сама функция find() тоже * вернет false. Возбуждает CException при обнаружении ошибок. */ virtual bool find(const std::string& dir, bool recur, const std::string& mask, FindFileCallback& cb)=0; /** * Обязательный виртуальный деструктор. */ virtual ~FindFile() {} private: /** запрещаем присваивание */ FindFile& operator=(const FindFile&); }; /** * Фабрика объектов. */ namespace Factory { /** * Создает объект, реализующий интерфейс FindFile. В случае возникновения * ошибок возбуждает CException. */ sh_ptr<FindFile> newFindFile(); } #endif |
Хорошо. Положим, файл мы нашли, прочитать сумеем, ну а с поиском текста (который, вообще говоря, может быть и регулярным выражением) что делать-то? Правильно, делать нужно интерфейс и поручать его реализацию соответствующим товарищам:
textfinder.hpp |
---|
/** @file * Определение интерфейса TextFinder. */ #ifndef __TEXTFINDER_HPP__ #define __TEXTFINDER_HPP__ #include "cexception.hpp" /** * Данный интерфейс предназначен для проверки содержит ли переданная строка * заданный текст. Предполагается, что текст задается в момент создания объекта, * что позволяет произвести всю необходимую инициализацию только один раз. */ class TextFinder { public: /** * Проверяет входит ли заданный текст в переданную строку. Возвращает * true, если найдено хотя бы одно вхождение. Возбуждает CException при * обнаружении ошибок. */ virtual bool findIn(const std::string& line)=0; /** * Обязательный виртуальный деструктор. */ virtual ~TextFinder() {} private: /** запрещаем присваивание */ TextFinder& operator=(const TextFinder&); }; /** * Фабрика объектов. */ namespace Factory { /** * Создает объект по переданному тексту, реализующий интерфейс TextFinder. В * случае возникновения ошибок возбуждает CException. */ sh_ptr<TextFinder> newTextFinder(const std::string& text); } #endif |
Ну, вроде бы и все, пора приниматься за работу.
/** @file * Реализация интерфейса FindText. */ #include "findtext.hpp"Включение заголовочного файла с объявлениями самым первым в файл с соответствующими реализациями является довольно полезной привычкой, т.к. позволяет отследить тот случай, когда данный хедер не является самодостаточным (например, не включает другие хедеры с необходимыми ему объявлениями).
#include <string.h> #include <errno.h> #include "findfile.hpp" #include "textfinder.hpp" #include "autofile.hpp" #include "stringbuf.hpp" using namespace std;Импортируем все определения из пространства имен
std
. Надеюсь, вы заметили, что ни один из заголовочных файлов не содержал подобной директивы? Это не случайно, т.к. действие подобной директивы уже невозможно отменить, и если кто-то поместил ее в хедер, то включая его в свой файл вы автоматически получаете то, что вредит вашему коду, но уже ничего не поделаешь.
namespace { /** вспомогательная структура для автоматической очистки флага */ struct AutoFlag { /** ссылка на флаг */ bool& flag; /** создает объект, контролирующий переданный флаг */ AutoFlag(bool& f) : flag(f) {} /** очищает флаг */ ~AutoFlag() { flag=0; } };Подобного рода вспомогательные структуры встречаются достаточно часто, но, как правило, не удостаиваются помещения в отдельный заголовочный файл ввиду своей излишней специфичности и упрощенной реализации.
struct OpenCloseSender; /** * Класс-реализация интерфейса FindText. */ class FindTextImpl : public FindText, FindFileCallback, Publisher<FoundMsg>, Publisher<InfoMsg>, Publisher<ErrorMsg> {Обратите внимание, что здесь используется реализационное (т.е.
private
) наследование сразу от нескольких классов.
friend struct OpenCloseSender; /** флаг, показывающий выполняется ли в данное время search() */ bool running; /** флаг, показывающий, что search() должна остановиться */ bool shouldStop; /** объект для поиска текста в строке */ sh_ptr<TextFinder> txtFnd; virtual void search(const std::string& dir, bool recur, const std::string& mask, const std::string& text); virtual void stopSearch(); virtual Publisher<FoundMsg>& foundPub(); virtual Publisher<InfoMsg>& infoPub(); virtual Publisher<ErrorMsg>& errorPub(); /** * Данная функция вызывается для каждого найденного файла. Она читает * содержимое файла, ищет заданную подстроку и посылает соответствующие * сообщения. */ virtual bool found(const std::string& fileName); /** * Читает строку текста из f в str. При обнаружении ошибки чтения * посылает сообщение ErrorMsg. Возвращает false по EOF или ошибке. */ bool readLine(FILE* f, string& str, const std::string& file); public: /** * Создает объект, устанавливая начальные значения флагов. */ FindTextImpl() : running(0), shouldStop(0) {} }; /** * Вспомогательная структура для автоматической рассылки сообщений * InfoMsg::opened и InfoMsg::closed. */ struct OpenCloseSender { /** ссылка на посылающий сообщения объект */ FindTextImpl& fti; /** имя файла */ const string& file; /** посылает сообщение InfoMsg::opened */ OpenCloseSender(FindTextImpl& fti_, const string& file_) : fti(fti_), file(file_) { fti.Publisher<InfoMsg>::send(InfoMsg(fti, InfoMsg::opened, file)); } /** посылает сообщение InfoMsg::closed */ ~OpenCloseSender() { fti.Publisher<InfoMsg>::send(InfoMsg(fti, InfoMsg::closed, file)); } };Еще один пример вспомогательной структуры, корректно выполняющей свою работу даже при возникновении исключений.
void FindTextImpl::search(const std::string& dir, bool recur, const std::string& mask, const std::string& text) { try { if (running) throw newCException(_FLINE_, "search() already running"); running=1; AutoFlag autoRunning(running); AutoFlag autoShouldStop(shouldStop); txtFnd=Factory::newTextFinder(text); sh_ptr<FindFile> ff=Factory::newFindFile(); ff->find(dir, recur, mask, *this); } catch (...) { throw newCException(_FLINE_, "Can't search", toCException(_FLINE_)); }Традиционный способ преобразования любого возникшего исключения в обещанное в спецификации
CException
(естественно, реальный тип возбуждаемого исключения -- это sh_ptr<CException>
). Используется один-единственный блок catch (...)
и уже внутри него исключение перевозбуждается, а полученная при этом информация возвращается функцией toCException()
в виде sh_ptr<CException>
. Данное исключение является причиной текущего, а по сему, оно передается ему в качестве nested
.
}Обратите внимание, что с использованием всех необходимых интерфейсов реализация
search()
становится подкупающе простой и изящной.
void FindTextImpl::stopSearch() { if (!running) throw newCException(_FLINE_, "search() isn't running"); shouldStop=1; } Publisher<FoundMsg>& FindTextImpl::foundPub() { return *this; } Publisher<InfoMsg>& FindTextImpl::infoPub() { return *this; } Publisher<ErrorMsg>& FindTextImpl::errorPub() { return *this; } bool FindTextImpl::found(const std::string& fileName) { try { if (shouldStop) return 0; AutoFILE f(fopen(fileName.c_str(), "r")); if (!f.get()) { Publisher<ErrorMsg>::send(ErrorMsg(*this, StringBuf("Can't open \"")+ fileName+"\": "+strerror(errno))); return 1; } OpenCloseSender ocs(*this, fileName); string str; str.reserve(1024); for (int lineNum=1; readLine(f.get(), str, fileName); lineNum++) { if (txtFnd->findIn(str)) Publisher<FoundMsg>::send(FoundMsg(*this, fileName, lineNum, str)); } return 1; } catch (...) { throw newCException(_FLINE_, "Can't process \""+fileName+"\"", toCException(_FLINE_)); } }Да и
found()
не намного сложнее.
bool FindTextImpl::readLine(FILE* f, string& str, const std::string& file) { if (feof(f)) return 0; str.clear(); for (int ch; ; ) { switch (ch=fgetc(f)) { case EOF: { if (ferror(f)) { Publisher<ErrorMsg>::send(ErrorMsg(*this, StringBuf("Can't read from \"")+file+"\": "+ strerror(errno))); return 0; } return (str.size()) ? 1 : 0; } case '\n': return 1; default: { str.push_back(ch); break; } } } }Вот только функции ввода/вывода, как правило, достаточно сложны. Что делать -- за полный контроль за потоком приходится платить.
} sh_ptr<FindText> Factory::newFindText() { try { return sh_ptr<FindText>(new FindTextImpl); } catch (...) { throw newCException(_FLINE_, "Can't create FindTextImpl object", toCException(_FLINE_)); } }Ну и в завершение, традиционная реализация соответствующей функции из фабрики объектов.
Создаваемый нами ИП будет разбирать параметры командной строки и выводить результаты поиска в stdout
. Кроме того, у пользователя будет возможность задать файл для вывода информации обо всех сгенерированных сообщениях, что весьма полезно для отладки. Ну а для аварийной остановки поиска вполне подойдет проверенный временем ^C
.
ftext.cpp |
---|
/** @file * Главный файл проекта ftext. Содержит функцию main() и все необходимые части * интерфейса пользователя. */ #include <stdio.h> #include "findtext.hpp" #include "stringbuf.hpp" using namespace std; /** * Данный класс предназначен для вывода на экран результатов поиска. */ class Finder : public Subscriber<FoundMsg>, public Subscriber<ErrorMsg> { /** * Выводит найденные строки. */ virtual bool regularMsg(const Publisher<FoundMsg>& pub, const FoundMsg& msg); /** * Выводит сообщения об ошибках. */ virtual bool regularMsg(const Publisher<ErrorMsg>& pub, const ErrorMsg& msg); }; bool Finder::regularMsg(const Publisher<FoundMsg>&, const FoundMsg& msg) { printf("%s:%d:%s\n", msg.fileName.c_str(), msg.lineNum, msg.lineText.c_str()); return 1; } bool Finder::regularMsg(const Publisher<ErrorMsg>&, const ErrorMsg& msg) { fprintf(stderr, "%s\n", msg.errMsg.c_str()); return 1; } /** * Данный класс предназначен для записи в лог информации по всем публикуемым * сообщениям. */ class Logger : public Subscriber<FoundMsg>, public Subscriber<ErrorMsg>, public Subscriber<InfoMsg> { /** лог для записи */ FILE* log; /** * Выводит найденные строки. */ virtual bool regularMsg(const Publisher<FoundMsg>& pub, const FoundMsg& msg); /** * Выводит сообщения об ошибках. */ virtual bool regularMsg(const Publisher<ErrorMsg>& pub, const ErrorMsg& msg); /** * Выводит информационные сообщения. */ virtual bool regularMsg(const Publisher<InfoMsg>& pub, const InfoMsg& msg); public: /** количество файлов, открытых во время поиска */ int filesOpened; /** количество файлов, закрытых во время поиска */ int filesClosed; /** количество найденных строк */ int linesFound; /** количество встреченных ошибок */ int errorsEncountered; /** * Открывает переданный файл для дописывания в него информации. */ Logger(const string& fname); /** * Дописывает в лог статистическую информацию и закрывает файл. */ ~Logger(); }; Logger::Logger(const string& fname) { log=fopen(fname.c_str(), "a"); if (!log) { throw newCException(_FLINE_, StringBuf("Can't open \"")+fname+ "\" file for logging"); } filesOpened=filesClosed=linesFound=errorsEncountered=0; } Logger::~Logger() { fputc('\n', log); #define PR(var) fprintf(log, #var ": %d\n", var) PR(filesOpened); PR(filesClosed); PR(linesFound); PR(errorsEncountered); #undef PR fputc('\n', log); fclose(log); } bool Logger::regularMsg(const Publisher<FoundMsg>& pub, const FoundMsg& msg) { linesFound++; fprintf(log, "FoundMsg: Publisher(%p) FindText(%p) fileName(%s) lineNum(%d) " "lineText(%s)\n", &pub, &msg.ft, msg.fileName.c_str(), msg.lineNum, msg.lineText.c_str()); return 1; } bool Logger::regularMsg(const Publisher<ErrorMsg>& pub, const ErrorMsg& msg) { errorsEncountered++; fprintf(log, "ErrorMsg: Publisher(%p) FindText(%p) errMsg(%s)\n", &pub, &msg.ft, msg.errMsg.c_str()); return 1; } bool Logger::regularMsg(const Publisher<InfoMsg>& pub, const InfoMsg& msg) { if (msg.type==InfoMsg::opened) filesOpened++; else filesClosed++; fprintf(log, "InfoMsg : Publisher(%p) FindText(%p) type(%s) fileName(%s)\n", &pub, &msg.ft, (msg.type==InfoMsg::opened) ? "opened" : "closed", msg.fileName.c_str()); return 1; } /** * Вспомогательная структура для разбора параметров коммандной строки. */ struct CmdLineParser { /** присутствует -l */ bool isL; /** имя лог-файла, если установлен isL */ string logName; /** присутствует -r */ bool isR; /** имя директория для поиска файлов */ string dirName; /** маска для поиска файлов */ string mask; /** подстрока для поиска в файлах */ string text; /** * Создает объект и инициализирует поля структуры. */ CmdLineParser() : isL(0), isR(0) {} /** * Разбирает коммандную строку формата * "[-l logname] [-r] dirname mask text" * и заполняет поля структуры. В случае некорректного формата возвращает * false. */ bool parse(int argc, char** argv); }; bool CmdLineParser::parse(int argc, char** argv) { if (argc<4 || argc>7) return 0; int curr=1; while (argv[curr][0]=='-') { string opt(argv[curr]); if (opt=="-l") { if (isL) return 0; isL=1; curr++; logName=argv[curr++]; } else if (opt=="-r") { if (isR) return 0; isR=1; curr++; } else return 0; } if (curr+3!=argc) return 0; dirName=argv[curr++]; mask=argv[curr++]; text=argv[curr++]; return 1; } /** * Выводит информацию о формате коммандной строки. */ void printUsage() { fprintf(stderr, "Usage:\n" " ftext [-l logname] [-r] dirname mask text\n" "For example:\n" " ftext -l ftext.log -r . *pp return\n" ); } int main(int argc, char** argv) { try { CmdLineParser clp; if (!clp.parse(argc, argv)) { // Некоторые системы/реализации вместо "*pp" сразу передают все файлы, // заканчивающиеся на "pp". Данный цикл печатает все полученные // параметры для диагностики подобной ситуации. // На таких системах/реализациях вместо "*pp" лучше передавать просто // "pp", т.е. без начальной "*". printf("Invalid command line:\n"); for (int i=0; i<argc; i++) printf("%s%s", argv[i], (i!=argc-1)? " " : "\n\n"); printUsage(); return 1; } sh_ptr<FindText> ft=Factory::newFindText(); Finder f; ft->foundPub().subscribe(f); ft->errorPub().subscribe(f); sh_ptr<Logger> l; if (clp.isL) { l.set(new Logger(clp.logName)); ft->foundPub().subscribe(*l); ft->errorPub().subscribe(*l); ft->infoPub().subscribe(*l); } ft->search(clp.dirName, clp.isR, clp.mask, clp.text); return 0; } catch (...) { fprintf(stderr, "Uncaught exception:\n%s", toCException(_FLINE_)->toStringAll().c_str()); return 2; } } |
Обратите внимание, что использующиеся реализации интерфейсов NameMatcher
(файл namematcherimpl.cpp
) и TextFinder
(файл textfinderimpl.cpp
) налагают определенные ограничения на вид маски и формат текста для поиска. Дело в том, что разработка более мощных реализаций не дала бы ничего нового в плане изучения интерфейсов и сообщений. И даже более того: тот факт, что пользователь может с легкостью включить в проект свои собственные расширенные реализации только улучшает материал.
Никакая часть данного материала не может быть использована в коммерческих целях без письменного разрешения автора.