Сообщество посвящено трюкам и красивым ходам в C++. Основателем и модератором являюсь я, mistificator. Читать далееПрошу оформлять записи в заданном мной стиле, либо просто посылать текст статьи на u-mail, и я размещу его с указанием авторства.
3. Имена конкретных классов в статье пишутся жирным шрифтом, имена абстрактных классов пишутся курсивом. Имена функций, переменных и методов пишутся обычным шрифтом.
//------------------------------------------------------------ "Для данного объекта нет заданных разрешений.
Предупреждение: это потенциально опасно, так как любой пользователь, имеющий доступ к данному объекту, может стать его владельцем. Владелец объекта должен как можно быстрее назначить разрешения." //------------------------------------------------------------
Comctl32.dll — важная деталь в механизме под названием Windows. Именно этой библиотеке мы обязаны кнопками и прочими элементами в характерном стиле XP. Common controls... Конечно, всё это уходит в прошлое, скоро XP отметит 10-летний юбилей, солидный возраст для софта.
Итак, привычный стиль XP. Если писать приложение с нуля, то стандартный функционал WinAPI не даёт программисту и такого стиля, выдавая на-гора оквадраченные кнопки с убогим системным шрифтом.
Что делать?
Читать дальше... Во-первых, статически прилинковать comctl32.lib к проекту, либо сделать одиночный вызов ::LoadLibrary("comctl32.dll"; до начала использования контролов. Во-вторых, прикрутить сей манифест:
В-третьих, разобраться с именами классов контролов, предоставляемых comctl32.dll. В этом нам поможет следующая таблица:
//------------------------------------------------------------ -------------------------------------------------------------------------- N Description Class Define -------------------------------------------------------------------------- 1. HEADER CONTROL SysHeader32 WC_HEADER 2. TOOLBAR CONTROL ToolbarWindow32 TOOLBARCLASSNAME 3. REBAR CONTROL ReBarWindow32 REBARCLASSNAME 4. TOOLTIPS CONTROL tooltips_class32 TOOLTIPS_CLASS 5. STATUS BAR CONTROL msctls_statusbar32 STATUSCLASSNAME 6. TRACKBAR CONTROL msctls_trackbar32 TRACKBAR_CLASS 7. UPDOWN CONTROL msctls_updown32 UPDOWN_CLASS 8. PROGRESS CONTROL msctls_progress32 PROGRESS_CLASS 9. HOTKEY CONTROL msctls_hotkey32 HOTKEY_CLASS 10. LISTVIEW CONTROL SysListView32 WC_LISTVIEW 11. TREEVIEW CONTROL SysTreeView32 WC_TREEVIEW 12. ComboBoxEx ComboBoxEx32 WC_COMBOBOXEX 13. TAB CONTROL SysTabControl32 WC_TABCONTROL 14. ANIMATE CONTROL SysAnimate32 ANIMATE_CLASS 15. MONTHCAL CONTROL SysMonthCal32 MONTHCAL_CLASS 16. DATETIMEPICK CONTROL SysDateTimePick32 DATETIMEPICK_CLASS 17. IP Address edit control SysIPAddress32 WC_IPADDRESS 18. Pager Control SysPager WC_PAGESCROLLER 19. Native Font Control NativeFontCtl WC_NATIVEFONTCTL 29. Button Control Button WC_BUTTON 21. Static Control Static WC_STATIC 22. Edit Control Edit WC_EDIT 23. Listbox Control ListBox WC_LISTBOX 24. Combobox Control ComboBox WC_COMBOBOX 25. Scrollbar Control ScrollBar WC_SCROLLBAR 26. SysLink control SysLink WC_LINK --------------------------------------------------------------------------
Для использования макросов требуется подключить commctrl.h.
Контрол RichEdit живёт отдельно от других, в разных версиях Windows — в разных библиотеках. RichEdit 1.0 находится в Riched32.dll, RichEdit 2.0 и выше в RichEd20.dll, RichEdit 4.1 и выше в Msftedit.dll.
Всё ещё мучаетесь сообщением типа "Entry point _Z5QFreePv not found in QtCore4.dll" при попытке запустить release-cборку приложения? Всё очень просто решается. В папке bin лежат QtCore4.dll и QtGui4.dll, собранные под Visual Studio 2008. И никаким боком они c вашим приложением, собранным на MinGW, работать не будут. Возьмите QtCore4.dll и QtGui4.dll (и что вам ещё нужно) из папки qt/lib , скопируйте в папку своего приложения, и будет вам счастье. Enjoy.
Давно имелись подозрения, что не всё гладко в реализации шаблонов в компиляторе Borland'a (bcc32, Borland C++ Compiler). Borland благополучно отошёл от разработки компиляторов, предоставив свои детища CodeGear'у. Те немного подретушировали компилятор, подправив в нём откровенные несоответствия современным стандартам С++ (добавив, в частности, поддержку неименованных union как в MSVC++), но некоторые проблемы с реализацией шаблонов всё же остались. Одна из них заключается в том, что приведенный ниже код не компилируется компилятором bcc32. Читать дальше...
//------------------------------------------------------------ template <class T, int Value> class A { public: union { T a[Value]; char b[Value * sizeof(T)]; } };
Компилятор заявляет, что невозможно вычислить sizeof(T) (хотя тип T известен на стадии компиляции), и что Value является неопределенным идентификатором. На самом деле, компилятор заблуждается, а проблема заключается в том, что он не может вычислить математическое выражение, подставленное в качестве размера массива. Оказывается, если вычислить выражение, объявив enum, то всё будет нормально. Так же, через объявление enum, решается проблема с "неопределенностью" Value. Перепишем код.
//------------------------------------------------------------ template <class T, int Value> class A { public: enum {__Value = Value }; enum {__Size = Value * sizeof(T)}; union { T a[__Value]; char b[__Size]; } };
Теперь код компилируется и выполняется. Приведенная выше проблема является одним из камней преткновения, вследствие которых Qt 4 не компилируется компиляторами bcc32.
Жесткая типизация в языке С++ подчас создаёт непреодолимые трудности. Например, вы, программист-разработчик, и занимаетесь реализацией некоего модульного проекта. Предположим, что это звуковой редактор с плагинами. Плагины вы реализуете в виде .dll, которые динамически подключаются при загрузке редактора. И всё бы ничего, но функции плагинов настолько разнообразны и разноплановы, что описать их в некоем формализованном виде просто нереально. В самом деле, представьте: один плагин принимает пять параметров, возвращает три; другой плагин принимает десять входных аргументов, и не выдает ни одного, зато пишет файл и рисует график на экране. Как быть в таком случае? Читать далее... У вас есть имя функции, которую нужно выполнить, и список параметров (типов), которые нужно передать функции. Достаточно ли этого в условиях жесткой типизации языка С++, чтобы выполнить функцию? Оказывается, вполне достаточно, если воспользоваться прямым помещением параметров в стек с помощью встроенного ассемблера, а по имени функции извлечь ее адрес из .dll с помощью WinAPI-функции ::GetProcAddress(). Другими словами, это позволяет избавиться от объявления функции непосредственно в коде программы, и вынести объявление функции во внешний конфигурационный файл. Приблизительный код универсального вызывателя функций приведен ниже. Первым идёт параметр-указатель на функцию вида void func(), массив Array содержит значения или указатели любых типов, приведенных к __int32 (то есть двойное слово, DWORD, определено как stack_cell_t), и Count содержит количество двойных слов. Входные данные в Array должны быть выровнены по 4-байтной границе.
В таком виде, как описано выше, функция call() будет возвращать 64-битное целое, которое может преобразовано в целое любого другого типа, либо в любой указатель. Если необходимо возвратить число с плавающей запятой, то строки
Разумеется, ResultFloat64 нужно объявить в функции call как double. P.S. Вообще, описанный материал достаточно обширный и предоставляет немалое поле для разного рода исследований и проверок. При необходимости, Куб 0 выложит дополнительную информацию и примеры в комментариях.
Иногда разработчику на С++ нужно остановиться и трезво оценить выполняемую задачу, оценить глубину своих знаний, оценить возможности STL и различных других библиотек, оценить сложность реализации элементарных парадигм средствами языка C++, и, осознав предстоящие трудности, перенести свой код в C#, например, или другой подходящий современный язык. Не будьте консерваторами!
Иногда разработчику класса нужно скрыть от пользователя некоторые особенности реализации интерфейса. Речь идет о секции private, в которой обычно размещаются переменные класса и методы, предназначенные сугубо для внутреннего использования. Пользователю вовсе необязательно видеть секцию private, особенно, если реализация класса находится внутри скомпилированной библиотеки .dll, а в руках пользователя лишь заголовочный файл. Читать далее...Предполжим, есть заголовочный файл следующего вида.
//------------------------------------------------------------ class A { public: A(char *, int); ~A(); private: int m_Value; char *m_Str; static int g_Instances; void init(); }; //------------------------------------------------------------
Существует достаточно простое решение для минимизации содержимого секции private (до двух строчек). Заголовочный файл оформляется следующим образом:
//------------------------------------------------------------ class A { public: A(char *, int); ~A(); private: class InternalData; InternalData *m_IData; }; //------------------------------------------------------------
Вот и всё. Объявляется новый класс, и создается переменная-указатель. Для ее создания компилятору не нужна реализация класса InternalData. Теперь как же выглядит файл с реализациями классов A и InternalData? Во-первых, добавилось тело класса InternalData, и, во-вторых, доступ к полям private осуществляется через m_IData.
//------------------------------------------------------------ class A::InternalData { public: int m_Value; char *m_Str; static int g_Instances; void init() { g_Instances++; } }; int A::InternalData::g_Instances=0; A::A(char *Str, int Val) { m_IData=new InternalData(); m_IData->m_Value=Val; m_IData->m_Str=Str; m_IData->init(); } void A::~A() { delete m_IData; } //------------------------------------------------------------
В устоявшуюся терминологию программистов C++ входят понятие раннего связывания (на стадии компиляции) и понятие позднего, отложенного связывания (динамически, во время исполнения). Далее в статье будет предложено решение для не просто позднего, а по-настоящему запоздалого связывания данных. Представьте, что Вы можете выбирать обработчик для данных прямо во время выполнения программы и, более того, ассоциировать с данными несколько обработчиков. Представьте, что при этом работает полиморфизм. Представьте, что если Вам более не нужны выдаваемые объектом данные, Вы можете "отключиться" от объекта, как клиент от сервера. Как такое реализовать? Читать далее...Со времен C известна методика использования указателей на функции. Это так называемый "косвенный вызов", используемый для организации callback-функций. В C++ на смену косвенным вызовам пришел объектно-ориентированный подход, но, что важно, косвенные вызовы все так же доступны для использования. Что они могут дать в C++? Прежде всего, некоторое нарушение инкапсуляции для классов, которое в малых дозах может оказаться полезным. Если условно назвать иерархию классов при наследовании "вертикальной", то использование косвенных вызовов позволяет получить "горизонтальную" иерархию. Для организации позднего связывания задействованы два класса — Signal и Wire. Класс Signal используется как базовый для создания классов-сигналов. Класс-сигнал является связующим звеном между отправителем и получателем данных. Сигнал может генерироваться где угодно, и получат его все подключенные к сигналу клиенты. Класс Wire используется для увязывания сигнала с клиентом-получателем (метод Connect()), и для генерации сигнала из требуемого места программы (метод Callback()). Для удобства использования основные вызовы объявлены через макросы. Макрос DECLARE_SIGNAL объявляет новый класс-сигнал. Это объявление может быть сделано как в глобальном пространстве имен, так и внутри класса. Макросы CONNECT_SIGNAL и DISCONNECT_SIGNAL устанавливают или разрывают связь с клиентом; в качестве клиента выступает метод класса. Макрос CALLBACK эмитирует класс-сигнал.
В программе, изобилующей мелкими объектами, иногда необходимо выяснить, сколько именно объектов создано, сотня или же миллион. А если объекты создаются и уничтожаются постоянно в течение жизненного цикла программы, то зачастую интересует вопрос, сколько раз были созданы объекты того или иного типа? Ключевые элементы такого счетчика объектов вырисовываются сразу: некие глобальные переменные и привязка к типам объектов. Сразу хочется сказать о подводных камнях, которые обнаруживаются в процессе организации счетчика. Дело в том, что классы как правило имеют наследование, поэтому простое инкрементирование счетчика в каждом конструкторе никуда не годится — при вызове конструктора будут инкрементированы и счетчики всех базовых классов. В приведенной далее реализации счетчика объектов используется соглашение, что базовый класс отмечается разработчиком. Для различения счетчиков разных объектов используется шаблонный класс, который специфицируется типом. (Возможно, альтернативным более компактным решением было бы специфицирование шаблона строкой, получаемой из typeid при подстановке this.) Счетчик, установленный в каком либо классе, обязательно должен быть установлен и во всех его производных классах, если только Вы не хотите, чтобы объекты производных классов подсчитывались в одной куче с объектами базовых классов. Читать дальше...
Счетчик устанавливается на объект класса в конструкторе вызовом метода Install. При установке счетчика на базовый класс необходимо вызвать метод Install с параметром true, что является для счетчика признаком базового класса.
//------------------------------------------------------------ class Base { public: Base() { InstanceCounter<Base>::Install(true); // пометка базового класса } }; class Derived: virtual public Base { public: Derived(): Base() { InstanceCounter<Derived>::Install(); } }; class Derived2: virtual public Base { public: Derived2(): Base() { InstanceCounter<Derived2>::Install(); } }; class Derived2_2: public Derived2 { public: Derived2_2(): Derived2() { InstanceCounter<Derived2_2>::Install(); } }; class SuperDerived: public Derived, public Derived2 { public: SuperDerived(): Derived(), Derived2() { InstanceCounter<SuperDerived>::Install(); } }; class Another { public: Another() { InstanceCounter<Another>::Install(true); // другой базовый класс } }; //------------------------------------------------------------ #include <iostream> int main() { Derived D1, D2, D3; Base B1, B2; Another A1, A2; Derived2_2 D2_21; SuperDerived SD1; std::cout << "Base #" << InstanceCounter<Base>::Value() << std::endl; std::cout << "Derived #" << InstanceCounter<Derived>::Value() << std::endl; std::cout << "Derived2 #" << InstanceCounter<Derived2>::Value() << std::endl; std::cout << "Derived2_2 #" << InstanceCounter<Derived2_2>::Value() << std::endl; std::cout << "SuperDerived #" << InstanceCounter<SuperDerived>::Value() << std::endl; std::cout << "Another #" << InstanceCounter<Another>::Value() << std::endl; return 0; } //------------------------------------------------------------
В примере создана некоторая иерархия классов, создано по несколько объектов каждого класса и результат подсчета количества объектов выведен на экран.
//------------------------------------------------------------ Base #2 Derived #3 Derived2 #0 Derived2_2 #1 SuperDerived #1 Another #2 //------------------------------------------------------------
Все правильно, и при обычном наследовании счетчик работает корректно. Счетчик корректно работает и при виртуальном наследовании. Для множественного наследования такой счетчик работает при условии, что иерархия классов имеет один базовый класс. В общем случае, для множественного наследования нужно искать другое решение, либо вовсе не устанавливать такой счетчик в базовые классы.
Thread-safe оптимизированный вариант счетчика, предложенный моим коллегой, сопряжен с использованием множественного наследования, и содержит три класса. Однако прост для понимания, и категорически рекомендован к использованию. Читать дальше...
В сфере программистов C++ существует устоявшийся термин "подсчет ссылок" (reference counting), обозначающий метод работы с объектами с использованием общих данных. Как только у данных появляется новый клиент, счетчик ссылок увеличивается на единицу, как только клиент завершает работу с данными, счетчик уменьшается на единицу. Когда последний клиент завершает работу с данными, данные уничтожаются. Зачем это нужно? Предположим, в Вашем проекте нужно обработать звуковые данные разными способами и затем сравнить результаты. Каждая процедура обработки считывает данные и приступает к работе. Если данные громоздки, разумно предоставить каждому клиенту ссылку на одни и те же данные, чтобы избежать их ненужного дублирования, но при этом вовсе не нужно давать понять клиентам, что они разделяют одни и те же данные. В идеальном случае клиент получает указатель на общие данные, и начинает работать с ним как с обычным указателем на объект, а в конце работы вызывает деструктор. Ниже предлагается шаблонный класс RefCounter, реализующий механизм подсчета ссылок. Его схема функционирования схожа с std::auto_ptr. Объект RefCounter ведет себя как указатель на класс, специализировавший шаблон, и позволяет выполнять операции присваивания и вызовы методов специализирующего класса. Читать дальше...
Важный аспект при использовании метода подсчета ссылок — неизменность разделяемых данных. Если один из клиентов желает изменить данные, тогда он должен получить себе локальную копию, и работать с ней, не мешая остальным. Для этого предусмотрен метод DeepCopy(). Так же стоит обратить внимание, что operator->() и operator*() сделаны константными, чтобы предупредить возможный вызов методов, изменяющих состояние объекта. Константность, конечно, не универсальное решение, зато простое, а статью растягивать на несколько страниц не хочется. Примерное использование класса RefCounter показано далее.
//------------------------------------------------------------ class SomeClass { public: SomeClass(int Num): m_Num(Num) {} void Method1() const {} // эти методы void Method2() const {} // не изменят объект private: int m_Num; // эти данные будут общими }; //------------------------------------------------------------ int main() { // используем подсчет ссылок для int* RefCounter<int> s; s=new int(5); RefCounter<int> s1; s1=s; // разделение данных между двумя клиентами int num1=*s.DeepCopy(); // так нельзя, ведет к утечке памяти int num2=*s1; // вот так безопасно // используем подсчет ссылок для SomeClass* const RefCounter<SomeClass> rc1=new SomeClass(12); RefCounter<SomeClass> rc2; rc2=rc1; // разделение данных между двумя клиентами rc2->Method1(); // примеры вызова метода класса SomeClass rc1->Method2(); (*rc2).Method1(); // разыменовывание и вызов RefCounter<SomeClass> rc3; rc3=rc1.DeepCopy(); // создание полной копии return 0; } //------------------------------------------------------------
Ни для кого не секрет, что С++, являясь языком общего назначения, а не ориентированным на определенную предметную область, во многих аспектах содержит методы лишь начального, базового уровня. В частности, не нашедшей должного отражения в синтаксисе языка является тема динамически масштабируемых многомерных массивов. Идея внести в программу двумерный масштабируемый массив вызовет у программиста тревогу, трехмерный — раздражение, а при мысли о четырехмерных и прочих n-мерных массивах программист С++, скорее всего, посоветует заказчику проекта ориентироваться на другой язык программирования. Однако, не все настолько мрачно. Хорошая новость заключается в том, что рекурсивное наследование шаблонов и перегрузка operator[] могут позволить изготовить класс, поддерживающий n-мерные массивы с возможностью их масштабирования, и всего того, что пожелает разработчик. Читать дальше...Для примера ниже приведен класс Array, позволяющий создавать массивы любого типа и любой размерности.
//------------------------------------------------------------ #include <string> #include <vector> #include <stdarg.h> #include <algorithm> template<class T, int Dim> class Array { // объявляем дружественным для вызова простого конструктора friend class std::vector< Array<T, Dim> >; // пользователю вряд ли нужно создавать массив без размера, // поэтому в private Array() { m_Data=0; } public: Array(int Sizes[Dim]) { Resize(Sizes); m_Data=0; } Array(int Size0, ...) { va_list Marker; va_start(Marker, Size0); int Sizes[Dim]; Sizes[0]=Size0; for (int Index=1; Index<Dim; Index++) { Sizes[Index]=va_arg(Marker, int); } va_end(Marker); Resize(Sizes); m_Data=0; } Array(const Array<T, Dim> &Ref) { m_Vector=Ref.m_Vector; m_Data=(Ref.m_Data) ? new T(*Ref.m_Data) : 0; } ~Array() { delete m_Data; } // динамическое масштабирование массива void Resize(int Sizes[Dim]) { if (Dim!=0) { m_Vector.resize(Sizes[0]); for (int Index=0; Index<Sizes[0]; Index++) { m_Vector[Index].Resize(Sizes+1); } } } Array<T, Dim> operator= (const Array<T, Dim> &Ref) { // перед присвоением сжимаем массив до нулевых размеров if (Dim!=0) { // ставим Dim+1, чтобы компилятор не выдавал ошибку при Dim==0 int Sizes[Dim+1]; std::memset(Sizes, 0, Dim*sizeof(int)); // будут вызваны деструкторы для всех элементов Resize(Sizes); } return Array<T, Dim>(Ref); } Array<T, Dim> &operator= (const T &Value) { if (Dim==0) { delete m_Data; // удаляем старое значение m_Data=new T(Value); // создаем новое } return *this; } Array<T, Dim-1> &operator[] (int Index) { // как и положено в С++, корректность индекса не проверяем return m_Vector[Index]; } operator T&() { // неявное преобразование типа, работает только для Dim==0, // иначе ошибка return *m_Data; } private: std::vector< Array<T, ((Dim-1)>0 ? (Dim-1) : 0) > > m_Vector; T *m_Data; }; // примеры использования int main() { Array<int, 2> data(7, 7); // двумерный массив int 7х7 int Sizes[2]={5, 5}; data.Resize(Sizes); // превращаем его в 5х5 data[1][2]=-1; // индексирование работает как для lvalue int x=data[1][2]; // так и для rvalue data[3][3]=11; x=data[3][3];
// копируем двумерный массив data в data2 Array<int, 2> data2(data); Array<int, 2> data3(2, 1); data3[0][0]=15; data3=data; // присваиваем data3 массив data
// четырехмерный строковый массив из 1 элемента Array<std::string, 4> str(1, 1, 1, 1); str[0][0][0][0]="xe"; // присваиваем значение
// некорректно, вызывает ошибку выполнения, так как массив двумерный // x=data[2]; // некорректно, вызывает ошибку выполнения // Array<char *, 4> Quad(data);
У многих есть мнение, что ресурсы, выделенные в потерпевшем крах конструкторе, очень трудно освободить, так как деструктор для объекта с незавершенным конструктором не вызывается, а указатели на уже инициализированные ресурсы теряются, что приводит к утечке памяти и прочим неприятностям. А что, если использовать не обычные указатели, а "умные" указатели? Например, стандартный std::auto_ptr? Если память для ресурсов организовать с помощью std::auto_ptr, то проблема освобождения ее снимается с плеч разработчика. Возьмем для примера два класса, Loser и Fixxer. Первый, в полном соответствии с названием, в конструкторе выделяет ресурс (память) и при передаче неподходящего параметра вылетает с исключением. Второй, так же в соответствии со своим названием, пытается выправить ситуацию. Посмотрим, как он с этим справляется. Читать дальше...
Если использовать обычные указатели, то в деструкторе Fixxer::~Fixxer() потребовался бы вызов delete m_Loser1 и delete m_Loser2 для вызова их деструкторов. Но ведь если исключение произошло в конструкторе Fixxer::Fixxer(), то объект не сконструирован, и для него никогда не будет вызван деструктор. Посмотрим на распечатку консольного вывода. Результат работы этой программы вполне удовлетворителен, хотя деструктор Fixxer::~Fixxer() и не вызывается.
Мы видим, что сначала вызываются оба конструктора Loser::Loser(). В конструкторе второго объекта происходит проверка условия, вызывается Cleanup() и выбрасывается исключение. А теперь посмотрим на строчки, выделенные курсивом. Видно, что для первого объекта вызывается Cleanup() ... из деструктора! Таким образом, использование std::auto_ptr позволяет выполнять корректное освобождение ресурса, инициализированного до возникновения исключения в конструкторе.
Классический синглетон — класс-одиночка — представляет собой класс, объект которого может быть создан один единственный раз за весь период выполнения программы, и который существует в течение всего времени выполнения программы. Реализовать его можно разными способами, о которых можно прочитать в литературных трудах Банды Четырех и ставшего притчей во языцех Коплиена. Однако, наиболее компактным, и не вызывающим головной боли при эксплуатации, является приведенный ниже вариант. Читать дальше...
//------------------------------------------------------------ class Service { private: Service() {} Service(const Service &) {} Service &operator=(const Service &) {} public: virtual ~Service() {} void Method1() {} void Method2() {} void Method3() {} static Service g_Instance; }; Service Service::g_Instance; //------------------------------------------------------------ // ниже приведен пример использования void Func1() { Service::g_Instance.Method1(); } void Func2() { Service::g_Instance.Method2(); } int main() { // раскомментируйте приведенные ниже строки // и убедитесь, что они не работают // Service local_Service; // Service another_local_Service(Service::g_Instance); // Service *ptr; *ptr=Service::g_Instance; Func1(); Func2(); Service::g_Instance.Method3(); return 0; } //------------------------------------------------------------
Предположим, экземпляр класса Service должен быть создан только один единственный раз. Для этого конструкторы делаются защищенными, защищенным делается так же оператор присваивания, а в открытом интерфейсе создается статическая переменная g_Instance (хотя и вопреки общепринятому обычаю включать в открытый интерфейс только методы, но не переменные, чтобы защитить их от несанкционированной модификации), являющаяся экземпляром самого класса. Очевидно, что g_Instance — это единственный случай, когда может быть вызван защищенный конструктор. Так же очевидно, что g_Instance не может быть модифицирована или перезаписана каким-то другим экземпляром класса Service, так как клиент класса лишен возможности самстоятельно вызвать конструктор.
Другим подходом к созданию синглетона является использование шаблона. Читать дальше...
//------------------------------------------------------------ template <class T> class SingletonKit { protected: SingletonKit() {} SingletonKit(const SingletonKit &); SingletonKit &operator= (const SingletonKit &); public: ~SingletonKit() {} static T g_Instance; }; template<class T> T SingletonKit<T>::g_Instance; //------------------------------------------------------------
Любой класс можно превратить в синглетон, используя SingletonKit, если соблюдать некоторые правила при написании класса, а именно: — создать закрытый конструктор, запретить конструктор копирования и присваивание; — сделать дружественным класс SingletonKit. Далее приводится пример тестового класса.
//------------------------------------------------------------ class Test { friend class SingletonKit<Test>; // SingletonKit объявлен дружественным private: Test() { m_Data=-1; } Test(const Test &); // запрет копирования Test &operator= (const Test &); // и присваивания int m_Data; public: ~Test() {} void SetData(int Data) { m_Data=Data; } int GetData() { return m_Data; } }; //------------------------------------------------------------
Теперь объект класса Test можно использовать как синглетон. Единственный объект доступен через SingletonKit<Test>::g_Instance.
//------------------------------------------------------------ int main() { SingletonKit<Test>::g_Instance.SetData(100); Test &Ref=SingletonKit<Test>::g_Instance; int Data=Ref.GetData(); return 0; } //------------------------------------------------------------
Применение конструкции с шаблонным классом позволяет "осинглетить" любой класс в уже существующей иерархии классов. Например, если есть иерархия классов Window — DialogWindow — StatusWindow, где последний класс написан разработчиком и должен управлять выводом статуса программы, то вполне можно сделать его синглетным, если требуется. Для этого нужно лишь удовлетворить требованиям к объявлению класса.
//------------------------------------------------------------ class StatusWindow: public DialogWindow { friend class SingletonKit<StatusWindow>; // SingletonKit объявлен дружественным private: StatusWindow() {} StatusWindow(const StatusWindow &); // запрет копирования StatusWindow &operator= (const StatusWindow &); // и присваивания public: ~StatusWindow() {} }; //------------------------------------------------------------
Общеизвестно, что деструктор объекта, если объект создан путем простого объявления, вызывается автоматически в конце блока. При творческом подходе автоматический вызов деструктора удается приспособить для выполнения специфических задач. Предположим, в разрабатываемом проекте необходимо измерять время выполнения тех или иных фрагментов кода или целых функций. Удобно подготовить класс TimeMeasure, не содержащий методов, а только лишь конструктор и деструктор. В конструкторе засекается время создания объекта TimeMeasure, в деструкторе вычисляется разность времен между выполнением деструктора и конструктора и выводится, например, на экран. Читать дальше...
//------------------------------------------------------------ #include <windows.h> #include <stdio.h> class TimeMeasure { public: TimeMeasure(const char *Comment="") { ::printf("%s (", Comment); m_Time=::GetTickCount(); } virtual ~TimeMeasure() { m_Time=::GetTickCount()-m_Time; ::printf("%i msecs)\n", static_cast<int>(m_Time)); } private: long long m_Time; }; // измерим время выполнения функции main() int main() { TimeMeasure T; // здесь вызывается конструктор TimeMeasure() return 0; // здесь автоматически вызывается деструктор ~TimeMeasure() } //------------------------------------------------------------
Видно, что процедура измерения времени сводится к написанию одной строчки кода. Максимум компактности, согласитесь.
Зачастую в классе необходимо объявить так называемые "свойства" — пары функций, отвечающие лишь за считывание и запись внутренней переменной. Например, SetColor() и GetColor(), SetText() и GetText() и так далее. Когда таких функций много, их объявление превращается в скучную рутинную работу, да и интерфейс изрядно загромождается. С помощью нехитрого макроса можно облегчить себе жизнь и сократить объем кода. Читать дальше...
//------------------------------------------------------------ #define prop(Type, Name) \ private: Type m_##Name; \ public: \ Type Get##Name () { return m_##Name; } \ void Set##Name(Type Name) { m_##Name=Name; } //------------------------------------------------------------ class SampleClass { // объявляем свойства с помощью макроса prop(int, Id); prop(std::string, Comment); prop(bool, CacheEnabled); // объявляем методы класса SampleClass(std::string Comment, bool CacheEnabled=false, int Id=-1) { SetComment(Comment); SetCacheEnabled(CacheEnabled); SetId(Id); } bool HasValidId() { if (GetId()<=0) { return false; } return true; } }; //------------------------------------------------------------
Как поступить, если под рукой имеется абстрактный класс, функциональность которого хочется задействовать, но наследовать свой класс от него неохота? Пусть имеется абстрактный класс окна AbstractWindow с чисто виртуальным методом Create().
//------------------------------------------------------------ class AbstractWindow { public: AbstractWindow() { // код конструктора } virtual ~AbstractWindow() { // код деструктора }
virtual int Create(const char *Title)=0 { // общий код создания окна } }; //------------------------------------------------------------
Предположим, что некий класс SimpleApplication, являющийся базовым классом для простых приложений, должен вызывать функцию создания окна. Таким образом, он должен включать в себя функциональность AbstractWindow. Открытое или закрытое наследование здесь нежелательно, так как логически SimpleApplication не связан с AbstractWindow — приложение не является частным случаем окна. Злоупотребляя механизмом наследования, можно создать производный от AbstractWindow производный класс SimpleWindow, имеющий в своем методе Create() весь необходимый арсенал для создания окна. Тогда экземпляр SimpleWindow можно было бы включить в интерфейс SimpleApplication и вызвать Create() в подходящий момент. Однако что, если для класса ComplexApplication, производного от SimpleApplication, понадобится новая реализация метода создания окна? Следует ли тогда создать производный класс ComplexWindow, открывая таким образом разработчику единственный путь — создание новых подклассов по мере создания новых видов приложений? Читать дальше...Возможно, такой путь оправдан, если глубина наследования от SimpleApplication невелика, а количество типов различных окон незначительно. В противном же случае удобнее воспользоваться предложенным ниже решением.
//------------------------------------------------------------ class SimpleApplication { friend class ConcreteWindow; private:
class ConcreteWindow: public AbstractWindow { public: ConcreteWindow(SimpleApplication *App): AbstractWindow(), m_App(App) {} virtual ~ConcreteWindow() {}
virtual int Create(const char *Title) { int Handle=AbstractWindow::Create(Title); m_App->OnWindowCreate(Title, Handle); return Handle; } private: SimpleApplication *m_App; };
ConcreteWindow *m_Window;
public: SimpleApplication(const char *Title) { m_Window=new ConcreteWindow(this); int Handle=m_Window->Create(Title); // остальной код конструктора } virtual ~SimpleApplication() { delete m_Window; } protected: virtual void OnWindowCreate(const char *Title, int Handle) { // код, выполняемый при создании окна } }; //------------------------------------------------------------
Здесь функциональная часть Create() из класса ConcreteWindow (который, кстати говоря, скрыт за ненадобностью из общей области видимости) фактически перенесена в виртуальный метод OnWindowCreate(), который может быть переопределен в подклассах. При этом гарантируется, что базовая часть кода AbstractWindow::Create() не будет забыта разработчиком и всегда будет выполнена. Таким образом, в приведенном примере показано, как вместо двух параллельных иерархий классов получить одну.
Ниже приведен пример "из жизни" с использованием описанной выше методики избегания наследования. В примере абстрактный класс зацикленного потока AbstractThread конкретизируется с использованием функционала Qt, посредством абстрактного же класса QThread, при этом не создается дополнительная иерархия классов и удается избежать множественного наследования.
//------------------------------------------------------------ class AbstractThread { public: AbstractThread(): m_Stop(false) {} virtual ~AbstractThread() {} virtual void Start()=0; void Stop() { m_Stop=true; Wait(); } virtual void Wait()=0; protected: void Run() { while (!m_Stop) { OnRun(); } m_Stop=false; } virtual void OnRun() {} private: bool m_Stop; }; //------------------------------------------------------------ #include <QThread> // Класс QThread абстрактный, содержит чисто виртуальный метод run(). // Ниже объявлен класс QtThread, использующий QThread // и производный только от AbstractThread class QtThread: public AbstractThread { friend class InternalQThread; private: class InternalQThread: public QThread { public: InternalQThread(QtThread *Thread): QThread(), m_Thread(Thread) {} virtual ~InternalQThread() {}