Крапелька рефлексії для С++. Частина перша: ретроспектива розробки



Створювати об'єкти за строковим іменами класів і отримувати інформацію про спадкоємців класів під час виконання програми. З++ або не підтримує, або слабо підтримує подібний функціонал «з коробки».

У цьому циклі статей буде я докладно розповідаю про те, як створював свою мікро-бібліотеку, що реалізує подібну поведінку і як готував її до публікації.

Посилання на всі статті циклу1. Про розробку
2. Про підготовку до публікації
3. Про результат




Моя совість не чиста, якщо я не зізнаюся в однієї хитрої думки, яка підштовхнула мене написати цей цикл статей. Так як це стосується, швидше, маркетингу, я використовую хованки під кат. Попереджаю: там досить багато тексту.

Розлоге вступ з саморазоблачением про піар і набитих шишкахЗайду здалеку…

Під час читання цих статей ви будете зустрічати згадка якоїсь «основний бібліотеки», частиною якої був cpprt. Я працюю над цією самою «основний бібліотекою» (у якої поки немає назви, тільки умовне data_mapping) чистим часом другий місяць. І з приводу «основний бібліотеки» у мене була одна дуже наївна думка. Я хотів на ній заробити.

Розумієте? Заробити на бібліотеці. Причому на утилитной бібліотеці. Причому продаючи її людям самостійно, а не через якийсь майданчик… Божевілля. План був наступний:

1. Створити якесь відносне стабільне ядро функціоналу, яке показувало б основні фічі бібліотеки на декількох ефектних прикладах.
2. Знайти людей, яких загоряться ідеєю проекту. Шукати планувалося серед знайомих (і через знайомих), на конференціях, через соціальні мережі, і т. д.
3. Знайшовши однодумців, ввести їх в курс справи за проектом та добитися того, щоб вони почали стабільно коммитить в репозиторій проекту.
4. Колективними силами довести проект до комерційного виду, по дорозі з'ясувати про механізми продажу (тільки тут, зверніть увагу, з'ясувати – свята простота!..) і почати продавати бібліотеку. Прибуток від продажів планувалося ділити в процентному співвідношенні на яких-небудь чесних, заздалегідь обговорених умовах між основними учасниками.
5. Після того, як підуть продажу, на правах засновника я сподівався делегувати завдання по підтримці і розвитку іншим учасникам (можливо, дещо знизивши при цьому свій відсоток від прибутку) і далі вкладати менше сил в бібліотеку і самому зайнятися своїми головними справами, маючи при цьому джерело більш або менш стабільного заробітку.
6. Ну і тут, як годиться, куди ж без нього… PROFIT!

Саме дивне, я серйозно вірив у працездатність цього плану.

Я вірив, що цей план, незважаючи на те, що мав п'ять років досвіду розробки своїх pet-проектів і знав – у людей є тенденція хотіти жити життя у позаробочий час, і це «хотіння жити життя» для дуже невеликої кількості знайомих (де-то для півтора знайомих, якщо бути чесним) означає «кодити всякі цікаві штуки на плюсах». Я знав, що заманити досвідчених колег по роботі в свої репозиторії неможливо, а підтягувати зацікавлених студентів до гідного рівня коммитера практично непосильне.

Я вірив, що цей план навіть після того, як поставив запитання і отримав відповідь у блозі користувача apofig (він же Сашко Баглай, спасибі йому). Суть відповіді зводилася, коротко, до того, що бібліотека, навіть якщо вона почне продаватися, це та ж робота – причому робота часом більш каторжна і нервова, ніж мирна оранка на дядька.

Я вірив, що цей план, справно працюючи над кодом два місяці поспіль. І тільки що стався два тижні тому, вже під час написання даного циклу статей, розмова по-справжньому відкрив мені очі на справжній порядок речей. Викладаю суть цієї розмови нижче, і дуже сподіваюся, що він виправдає настільки довге і розлоге вступ:

Відповідь Юрія Рощенко

Людини, який розставив всі крапки над i, звали Юрій Рощенко. Він працював керівником відділу у великій міжнародній аутсорсингової компанії і добре розбирався у веденні проектів взагалі. Юрій витратив хвилин сорок свого часу на розмову зі мною, за що я йому дуже вдячний.

Виклавши свій план по розробці бібліотеки, я отримав однозначну відповідь: ні, так не працює. Діяти потрібно зовсім по-іншому. І Юрій розповів як. Поєднавши відповідь Юрія Рощенко і деякі поради від Олександра Баглая, я сформулював наступні кроки розвитку проекту:

1. Оформити мінімальний репозиторій проекту з описом і демонстрацією його можливостей. При цьому важливо, щоб проект запускався без складного налаштування, в один клік, взагалі без участі вас як автора та джерела підказок.

2. Показати проект максимально широкому колу знайомих: колегам в компаніях, де ви працювали, друзям, і т. д. Дати доступ до сховища, запропонувати випробувати проект. Це те, що у apofig було позначено словами «дай йому [користувачеві] інструмент у руки, закрий рот і дивися, що він з ним буде робити».

3. Наступний пункт став для мене одкровенням. За словами Юрія, якщо проект буде позитивна реакція знайомих, потрібно – увага! – викотити його на open source. Як можна?!.. Я завжди трусився над своїми досягненнями аки Кощій Безсмертний над златом. Думка так от запросто віддати плоди своїх праць звучала для мене крамолою. «Вкрадуть!» – думав я. «Вкрадуть, сплагиатят!» – думав я.
Ні, відповів мені Юрій. Все буде добре… Чому все буде добре, він пояснив далі.

4. Після публікації проекту open source, потрібно максимально його розпіарити. Більше розголосу – більше користувачів, більше відловлених багів, більше дійсно корисних фичей, викликаних безпосередньо від спробували проект людей. Кількість за Гегелем має тенденцію переростати в якість, а користувачі – контрибуторів. Більше контрибуторів – ще вище якість проекту, ще більше зірок. Більше зірок, вище позиція проекту на репозиторії, а значить більше розголосу і… коло замикається. І в даному випадку те, що коло замикається – дуже добре.
Примітка: Якщо не ясно, хто такі контрибуторы – не біда. Ось посилання на невелику статтю про ролях користувачів GitHub. Контрибуторы – це такі хороші люди, які вносять пропозиції про зміни (pull request) в ваш проект.

5. У якийсь момент проектом можуть почати користуватися комерційні компанії. Потрібно дізнаватися, хто користується, потрібно писати назви компаній в якості реклами – адже якщо продуктом користуються проекти, які приносять прибуток своїм авторам, це означає, що продукт сам по собі теж досить цінний.
І тільки в цей момент має сенс створити закритий форк зі свого відкритого репозиторію і можна спробувати продавати його під комерційною ліцензією, приправивши безкоштовну версію якими-небудь додатковими корисними і потрібними фічами.
Як власник, ви будете мати доступ до інформації, дуже корисною для комерціалізації бібліотеки: до статистики завантажень, актуальні питання та паттернам використання коду, до різних пропозицій, які можуть надходити до вас як до власника проекту від користувачів безпосередньо.
В контексті розмови Юрій саме тут відповів на питання, чому бібліотеку не вкрадуть і не сплагиатят. Справа в тому, що, як засновник, ви завжди будете розбиратися в проекті глибше, ніж більшість контрибуторів. Щоб потенційним зловмисникам розібратися в проекті і написати схожий код, їм доведеться витратити прірву часу і сил або, кажучи по-іншому – грошей. А це, в свою чергу, означає, що цим ніхто не буде займатися до тих пір, поки купівля бібліотеки не стане значно дорожче вкладень в розробку її клону.

6. Якщо проект стане-таки астрономічно відомим, і ви відчуєте, що навіть який-небудь Google хоче таку саму штуку, як у вас – варто спробувати продатися. Продатися – це успіх, це добре. Грамотно поторгувавшись, можна виручити з продажу стільки грошей, скільки самим навряд чи заробити з цієї бібліотеки за все життя.
При цьому якщо опиратися, то є реальний ризик, що який-небудь Google напише протягом тижня таку ж бібліотеку, як у вас, і ви залишитеся біля розбитого корита.

По пунктах, здається, все. Додам ще кілька зауважень від Юрія:
– Потрібно пам'ятати про дві поворотні точки розвитку проекту: (1) момент, коли має сенс зробити комерційний форк, і (2) момент, коли для кого-небудь купити проект стане дорожче, ніж написати самому.
– Хабра – дійсно хороший майданчик для популяризації проекту в російськомовному сегменті інтернету. Мій співрозмовник згадав тут хлопці з PVS-studio, які просувають свій продукт на хабре, одночасно розповідаючи людям корисні речі.
– Важливо пам'ятати про англомовний сегмент – і орієнтуватися в чому на нього теж.

У загальному і цілому, я виклав тут всі основні думки, які виніс з розмови. Сподіваюся, кому-небудь допоможе представлений алгоритм. І, сподіваюся також, хто-небудь поділиться в коментарях своїм досвідом і своїми думками з даного приводу. Упевнений, це буде корисно для всього співтовариства.

… так, трохи не забув. Я хотів зізнатися в одній хитрощі.

Справа в тому, що даний цикл статей стосується дуже невеликий, але, на мій погляд, самодостатньої частини «основний бібліотеки», отщипнутой від нього і перетвореної в бібліотеку. На її прикладі я вирішив спробувати, як це – просувати і публікувати ПЗ з відкритим вихідним кодом. Пройти таким чином шлях, який належить пройти потім для «основний бібліотеки»… При цьому, природно, я чесно готовий підтримувати саму бібліотеку cpprt, якій присвячений даний цикл статей, якщо вона когось зацікавить.

Можливо, хтось може порахувати мою задумку маніпуляцією. Можливо… Але я виправдовую себе тим, що намагаюся тут максимально детально розповідати про свій досвід, що може бути цінним для людей, які захочуть коли-небудь вирішувати аналогічні завдання.
Сподіваюся, це окупить час, який ви, дорогі читачі, витратите на ознайомлення з даними циклом статей.


Даний цикл статей складається з трьох частин:

Перша частина, ретроспективна. У ній докладно викладається історія розробки мікро-бібліотеки.
Навіщо читати: Ця частина призначена для людей, яким цікаво, які можна набити шишки в процесі написання коду для реєстрації інформації про класах до старту функції main(). Серйозним програмістам С++ ця частина може здатися наївною і не цікавою.

Друга частина, публикационная. Розповідає про підготовку сховища бібліотеки до публікації: вибір ліцензії, організація структури репозиторію, вивчення CMake, і т. д.
Навіщо читати: Ця частина може бути корисна людям, у яких припадає пилом на диску своя бібліотека і які хочуть дізнатися, як можна уявити цю бібліотеку людям.

Третя частина, документационная. Тут дається погляд на бібліотеку з позиції користувача: основні use cases використання, механізм реєстрації класів, API для створення об'єктів за строковим імен класів, API для доступу до інформації про успадкованих класах через вказівку батьківського класу під час виконання програми. Ця частина містить приклади коду, а також плани на майбутнє.
Навіщо читати: Ця частина може бути корисна людям, охочим отримати надається cpprt функціонал у своєму проекту, а також, знову, для бажаючих внести свою лепту в розвиток бібліотеки і, можливо навіть, у розвиток «основний бібліотеки».

За сім я закінчую про циклі, і переходжу до суті конкретно даної статті.

0. Вступ

Структура даної статті:

Розділ №0. Даний розділ. Тут розповідається про аналоги бібліотеки cpprt і даються деякі думки з приводу того, чому cpprt має право на існування при наявності серйозних аналогів.
Розділ №1. Навіщо взагалі знадобилася рефлексія в С++.
Розділ №2. Про пошук рішення для виниклої задачі.
Розділ №3. Про першу реалізацію механізму реєстрації класів.
Розділ №4. Про те, які проблеми були в першому рішенні про те, як я їх виправляв.
Розділ №5. Додавання можливості реєструвати інформацію про спадкування.
Розділ №6. Висновок.

І тепер, нарешті, починаємо розмову.

На момент публікації даного циклу статей моя бібліотека дозволяє дуже небагато:
1. Створювати об'єкти по строковому імені їх класів.
2. Отримувати інформацію про спадкоємців класів в runtime.

Вже при роботі над статтею знайомий підкинув посилання на огляд існуючих бібліотек. У ньому згадані дійсно потужні проекти. Вони дозволяють оснащувати класи З++ товстенним шаром метаінформації і пов'язаного з цим функціоналу: створювати об'єкти за строковим імен класів, отримувати перелік полів і методів класів під час виконання програми, викликати ці методи… Коротше, дозволяють купатися в цілих океанах метаінформації. Мої досягнення здавалися нікчемними в тіні конкурентів. Однак, трохи заспокоївшись, я подумав: можливо, певний сенс розповісти про виконану мною роботу є.

По-перше, в даних статтях багато уваги приділяється ретроспективі: з якими труднощами я стикався в процесі роботи, як я їх вирішував. Це може бути корисно в якості навчального матеріалу.

З іншого боку, даний цикл розповідає про те, як я готував бібліотеку до публікації: досліджував ліцензії, розбирався з CMake, з світом відкритого ПЗ і з тим, як прийнято оформляти свої проекти в цьому світі. Це теж може комусь стати в нагоді.

Ну і, нарешті, так, нехай моя мікро-бібліотека дійсно дуже обмежена в можливостях у порівнянні з існуючими зубрам рефлексії С++. Але, можливо, в цьому полягає її перевага. Чим менше бібліотека – тим легше користувачеві усвідомити особливості її роботи і тим легше її інтеграція в проект. Потрібна потужна рефлексія – використовуйте потужну бібліотеку. Потрібен набір з декількох простих можливостей – можна спробувати cpprt.

1. Навіщо мені знадобився подібний функціонал

С++ відомий своєю ефективністю. Код, написаним на ньому, виходить добре оптимізованим передусім можлива завдяки можливості тонкого налаштування використання ресурсів комп'ютера. С++ — дійсно дозволяє писати дуже добре оптимізований код. Але за це потрібно платити.

На вівтар ефективності, крім усього іншого, кладеться можливість використання деяких метаданих під час виконання програми. Так, є RTTI з його typeid і dynamic_cast. Boost.TypeTraits. Але RTTI часто відключають для економії ресурсів (посилання з приводу), а Boost.TypeTraits, будучи бібліотекою, побудованої на шаблонах, не особливо дружить з логікою часу виконання програми і породжує багато службових спеціалізацій своїх шаблонів.

При цьому іноді без можливості створення класу за його строковому імені не обійтися. Мені, наприклад, така можливість знадобилася в рамках механізму серіалізації: для збереження і завантаження об'єктів, що зберігаються за вказівниками на батьківський клас. Звучить, можливо, не дуже зрозуміло. Поясню докладніше.

Нехай у нас є система для збереження стану об'єктів класів. Нехай використання її виглядає якось так:

// В рамках реалізованої схеми інтерфейс повинен ISerializable
// реалізовувати будь-клас, який можна сериализировать
class ISerializable {
virtual void save(Serializer *inSerializer) = 0;
virtual void load(Deserializer *inDeserializer) = 0;
}

class Base : public ISerializable {
virtual void save(Serializer *inSerializer) { /* збереження стану об'єкта через inSerializer */ }
virtual void load(Deserializer *inDeserializer) { /* завантаження стану об'єкта через inDeserializer */ }
};

class Derived : public Base {
virtual void save(Serializer *inSerializer) { /* збереження стану об'єкта через inSerializer */ }
virtual void load(Deserializer *inDeserializer) { /* завантаження стану об'єкта через inDeserializer */ }
};


Тестовий код, який зберігає/завантажує дані:

Base *theBase = new Derived();
. . .
Serializer theSerializer("save.txt");
theSerializer.save("name", theBase);
theSerializer.flush();
. . .
Deserializer theDeserializer("save.txt");
theBase = theDeserializer.load("name");


При збереженні об'єкта, який зберігається за вказівником на батьківський клас, сериализатор викличе метод save(...) спадкоємця за рахунок поліморфізму. Але при завантаженні десериализатор повинен як-небудь дізнатися, який саме клас був у об'єкта в момент збереження, щоб мати можливість створити його.

Звідси випливає, що на етапі збереження потрібно мати якийсь ідентифікатор класу об'єкта, за яким на етапі завантаження ми зможемо дізнатися, об'єкт якого класу треба створити і, скориставшись цим ідентифікатором, через якесь API створити об'єкт потрібного класу.

Ця ж думка у вигляді коду:

class Serializer {
void saveInt(const char *inName, int inValue) { ... }
void saveString(const char *inName, const std::string &inValue) { ... }
// . . .

// Тут нам допоможе поки поліморфізм – буде викликана реалізація
// методу save( ) у спадкоємця за рахунок віртуальності методу save(...)
void saveObject(ISerializable *inObject) { inObject->save(this); }
};

class Deserializer {
int loadInt(const char *inName) { ... }
void loadString(const char *inName, std::string &outValue) { ... }
// . . .

ISerializable *loadObject() {
// А ось тут потрібно якось дізнатися клас, який
// був у збереженого об'єкту – інакше не ясно, об'єкт
// якого класу створювати
ISerializable *theObject = new < ??? >( )
theObject->load(this);
return theObject;
}
};


Висновок: код методів для збереження об'єктів повинен бути розширений з використанням якого-небудь API. Час прототипировать!



void saveObject(ISerializable *inObject) {
// Зберігання ідентифікатора, що задає тип зберігається об'єкта
this->saveObjectType(inObject->classID()); // <<<--- classID()
inObject->save(this);
};

ISerializable *loadObject() {
// Створення об'єкта через фабрику на основі ідентифікатора завантажуваного
ISerializable *theObject = objectFabric().create(
this->loadObjectType());// <<<--- objectFabric().create(...)
theObject->load(this);
return theObject;
};


Накидавши таким чином загальний вигляд потрібного API, я взявся за пошук рішення, яке надавало б подібний функціонал.

2. Пошук рішення

Для початку я погуглив, щоб дізнатися, що з приводу потрібної поведінки думають люди. Хотілося чогось легкого, без тисячі залежностей, без використання допоміжних систем на зразок Qt MOC (стаття на тему) і, крім усього іншого, простого у використанні. Знайти готове рішення у вигляді бібліотеки, що задовольняє вказаним критеріям, не вдалося.

ПриміткаЯк я писав вище, вже під час написання статей з'явилася посилання на огляд існуючих якісних рішень (ось вона ще раз). Дуже дивно, що google за запитом «c++ reflection» видавав першої посилання на дивну статтю «Мій велосипед до reflection в c++», а не на цю відповідну публікацію.


Я почав думати, як реалізувати подібний функціонал самостійно.

Рішення, у загальному і цілому, крутилися навколо опису фабричних функцій та/або класів, які реєструвалися і використовувалися далі через якийсь механізм, який вибирав потрібну фабрику в залежності від переданого імені класу. В рамках даної статті я вирішив виділити ось цей відповідь на stackoverflow (авторства Johannes Schaub, спасибі йому за ідею), так як в ньому втілилися основні підходи, знайдені в мережі.

Перша ідея, запропонована Johannes Schaub, передбачала використання шаблонної фабричної функції та збереження її спеціалізацій в словнику. Ідея сама по собі мені не дуже сподобалася – механізм виглядав як щось завершене, без красивої об'єктної обгортки. Тим не менш, деякі відгомони такої реалізації можна виявити в отриманій фінальної реалізації cpprt (використання фабричних класів).

Друга ідея від Johannes Schaub: включення механізму реєстрації фабрик у конструктори класів, які [конструктори] викликалися при створенні статичних об'єктів цих класів. Ця ідея мені сподобалася, особливо з урахуванням запропонованих макросів: макроси дозволяли приховувати деталі механізму реєстрації, за рахунок чого цей механізм можна було б змінювати від версії до версії бібліотеки без побічних ефектів для користувачів коду.

Наводжу тут рішення Johannes Schaub з мінімальними змінами і зі своїми коментарями:

Рішення від Johannes Schaub
// in base.hpp:

// Шаблонна фабрична функція. Створює об'єкт довільного типу.
// Тут згадується якийсь клас Base, який Johannes, як я розумію,
// розглядає в якості базового для всіх реєстрованих класів.
// У принципі, з фабричної функції можна було б повертати покажчик 
// недійсним, узагальнивши, таким чином, рішення.
// Але якщо виділити певний базовий реєстрований клас Base, то можна
// зробити механізм реєстрації типів більш зручним
|| (про що буде розказано далі).
template < typename T > Base * createT() { return new T; }

// Базовий клас для "реєстраторів" фабричних функцій
struct BaseFactory {

// Словник, за допомогою якого ми зможемо отримувати покажчик
// на спеціалізацію фабричної функції по строковому імені
// класу. Такий словник можна створити за рахунок того, що всі
// спеціалізації функції createT мають однакову сигнатуру
// (повертають однаковий тип і відрізняються тільки реалізацією).
typedef std::map< std::string, Base*(*)() > map_type;

// Метод для створення об'єктів за строковим іменами
// класів... Думаю, тут все і так ясно.
static Base *createInstance(std::string const& s) {
map_type::iterator it = getMap()->find(s);
return (it == getMap()->end()) ? NULL : it->second();
}

protected:
// Цей метод, а також наступний покажчик-статична
// змінна map реалізують разом шаблон сінглтон. У цьому
// синглтоне зберігається глобальний словник, що зв'язує
// фабричні функції із рядковими іменами.
static map_type * getMap() {
// Коментар Johannes Schaub:
// never delete'ed. (exist until program termination)
// because we can't guarantee correct destruction order 
if(!map) { map = new map_type; } 
return map; 
}

private:
static map_type * map;
};

// Клас, в конструкторі зв'язує спеціалізацію фабричної
// функції з рядковим ім'ям. Фактично, виклик конструктора
// даного класу – це все, що потрібно, щоб для класу T можна було
// виконувати конструювання об'єктів по строковому імені класу.
template < typename T >
struct DerivedRegister : BaseFactory { 
DerivedRegister(std::string const& s) { 
getMap()->insert(std::make_pair(s &createT< T >));
}
};

// in derivedb.hpp

// Тестовий клас, який реєструється через механізм,
// створений Johannes Schaub. Наскільки я розумію, Johannes Schaub
// забув успадкувати його від Base. Я виправив це:
class DerivedB : public Base {
...;
private:

// Тут ми маємо предекларацию статичної змінної reg.
static DerivedRegister< DerivedB > reg;
};

// in derivedb.cpp:

// Тут (у файлі, де розташована реалізація класу DerivedB)
// описується сама статична змінна, в конструкторі якої
// виконується зв'язування фабричного методу з рядковим ім'ям.
// Під час статичної ініціалізації буде викликаний конструктор
// спеціалізації DerivedRegister< DerivedB > і пройде зв'язування
// фабричної функції з ім'ям DerivedB
DerivedRegister< DerivedB > DerivedB::reg("DerivedB");



Johannes Schaub запропонував також макроси для спрощення реєстрації типів. Вони дозволяють закрити особливості реалізації системи реєстрації і роблять код лаконічніше:

Макроси від Johannes Schaub
#define REGISTER_DEC_TYPE(NAME) \
static DerivedRegister<NAME> reg

#define REGISTER_DEF_TYPE(NAME) \
DerivedRegister<NAME> NAME::reg(#NAME)



Я взяв рішення Johannes Schaub за основу, трохи змінивши його за своїм смаком.

3. Перша реалізація

У вирішенні Johannes Schaub був шаблонний клас DerivedRegister, об'єкти спеціалізацій якого створювалися як статичні поля в рамках реєстрованих класів (static DerivedRegister reg). Першим ділом я вирішив перенести фабричні функції в клас DerivedRegister як фабричних методів. За рахунок цього, крім спрощення коду, з'являлася можливість розширювати метаінформацію про реєстрованих класах простим додаванням полів в клас DerivedRegister.

Також я переніс інформацію про строковому імені класів у свій аналог DerivedRegister, розпочавши таким чином використовувати його, як сховище метаінформації (поки в якості метаінформації виступало тільки рядковий ім'я класу).

Вийшла, фактично, реалізація шаблону проектування абстрактна фабрика з деякими наворотами для можливості виконання коду реєстрації метаданих до старту функції main():

ClassManager.h
// Узагальнений менеджер класів (аналог DerivedRegister з рішення
// Johannes Schaub). Містить в собі типову для будь-якого класу
// метаінформацію. Вимагає від спадкоємців реалізовувати фабричний
// метод createAbstractObject()
//
class IClassManager {
private:
const char *_name;

public:
IClassManager(const char *inClassName) : _name(inClassName) { }

//-- Workflow
const const char *name() const { return _name; }
virtual IManagedClass *createAbstractObject() = 0;
};

//-----------------------------------------------------------------
// Реалізація менеджера класів. Реалізує фабричний
// метод і бере на себе завдання реєстрації самого себе
// в системі реєстрації метаінформації про класах.
// Реєстрація виконується в рамках конструктора.
template < typename T_Type>
class ClassManager : public IClassManager {
public:
// Реєстрація фабрики виконується, як і в коді
// Johannes Schaub, ось тут, в конструкторі:
ClassManager(const char *inClassName) : IClassManager(inClassName) {
globalRuntime.registerClass(this);
}

T_Type *createObject() { return new T_Type(); }
virtual IManagedClass *createAbstractObject() { return createObject(); }
};



Змінна globalRuntime – це глобальний об'єкт, який виділений для управління всією метаинформационной машинерией. Фактично, просто об'єктна обгортка навколо вектора, зберігає об'єкти спеціалізацій ClassManager.

Розглянемо код класу, об'єкт якого – це globalRuntime. Думаю, суть його роботи буде зрозуміла без коментарів:

CPPRuntime.h
class CPPRuntime {
private:
std::vector< IClassManager * > _registries;

IClassManager *managerByName(const char *inName);

public:
CPPRuntime() : _registries() { }

void registerClass(IClassManager *inClass);
IManagedClass *createObject(const char *inClassName);
};

//-----------------------------------------------------------------------------
extern CPPRuntime globalRuntime;



CPPRuntime.cpp
IClassManager *CPPRuntime::managerByName(const char *inName) {
for (size_t theIndex = 0, theSize = _registries.size();
theIndex < theSize; ++theIndex)
{
if (0 == strcmp(_registries[theIndex]->name(), inName)) {
return _registries[theIndex];
}
}
return NULL;
}

//-- Registering
void CPPRuntime::registerClass(IClassManager *inClass) {
_registries.push_back(inClass);
}

//-- Public API
IManagedClass *CPPRuntime::createObject(const char *inClassName) {
IClassManager *theRegistry = managerByName(inClassName);
//TODO: Through an exception if no class found
return theRegistry->createAbstractObject();
}

//-----------------------------------------------------------------------------
CPPRuntime globalRuntime;



Залишалося описати базовий клас IManagedClass (аналог класу Base з рішення Johannes Schaub) і створити макроси для спрощення механізму реєстрації класів.

У зв'язку з описом базового класу IManagedClass, слід згадати API, заради якої все затівалося:

1. API для завантаження даних. Можливість створювати об'єкт з кодом:
objectFabric().create(inObjectID)
Це готове, метод globalRuntime.createObject(«ClassName»).

2. API для збереження даних. Можливість отримувати ідентифікатор класу об'єкта:
object->classID()
Рішення, запропоноване Johannes Schaub, не включало в себе таке API. Його доведеться реалізовувати самостійно.

Я накидав use case для наслідування IManagedClass на прикладі одного класу. Думаю, тут теж все більш або менш ясно без зайвих коментарів:

TestClass.h
//---------------------------------------------------------------------------------------------------
// Базовий клас, з яким взаємодіє розроблювана система.
// Задає принципи отримання метаінформації для всіх об'єктів
// зареєстрованих класів.
class IClassManager {
public:
virtual IClassManager *getRegistry() = 0;
};

//---------------------------------------------------------------------------------------------------
// Клас користувача, наступний базовий клас. Користувач повинен
// успадковувати базові класи своїх ієрархій від базового класу щоб
// мати можливість прив'язувати до них метадані і отримати доступ
// до метаданих під час виконання програми.
class TestClass : public IClassManager {
public:
// Службовий код для реєстрації класу. Його варто закрити за макросом.
static ClassManager< TestClass > gClassManager; 
virtual IClassManager *getRegistry() { return &gClassManager; }
public:
};



TestClass.cpp
// Виклик конструктора об'єкта gClassManager повинен зареєструвати
// фабрику для створення екземплярів класу TestClass по строковому
// імені цього класу. Даний код теж варто було б закрити за макросом.
ClassManager< TestClass > TestClass::gClassManager("TestClass");



Я запустив код з використанням цього класу, а саме, отдебажил виклик globalRuntime.createObject(«TestClass»). Об'єкт благополучно створився.

Залишалося описати макроси, які зняли б з користувача необхідність вручну робити копі-пасту коду для реєстрації класів:

Macros.h
#define CPPRT_DECLARATION(M_ClassName)\
public:\
static ClassManager< M_ClassName > gClassManager;\
virtual IClassManager *getClassManager() { return &gClassManager; }\
protected:\

// Що приємно, це рішення буде працювати в тому числі для класів, вкладених у namespace.
#define CPPRT_IMPLEMENTATION(M_ClassName) ClassManager< M_ClassName > M_ClassName::gClassManager(#M_ClassName);


Примітка: Тут вперше згадується префікс CPPRT. Це скорочення від слів C Plus Plus Run Time.



Макрос був готовий. Принцип його використання не відрізнявся від принципів використання макросу, запропонованого Johannes Schaub:

Принцип використання макросуНаприклад, ми хочемо зареєструвати клас TestClass. Декларуємо клас:

//--- TestClass.h ---
class TestClass : public IClassManager {
// Цей макрос найкраще прописувати на початку декларації
// класу – щоб гарантовано не поміняти спецификатор
// доступу, що задається на початку декларації класів як protected
CPPRT_DECLARATION(TestClass)
. . .
};


Далі створюємо файл реалізації. В файлі реалізації використовуємо макрос для статичного опису об'єкта, що зберігає інформацію про клас CPPRT_IMPLEMENTATION (згадуємо, через макрос CPPRT_DECLARATION ми цей об'єкт лише декларували).

//--- TestClass.cpp ---
CPPRT_IMPLEMENTATION(TestClass)



Готово! Тремтячи від нетерпіння, я обв'язав описаними макросами ієрархію своїх класів, реалізував методи серіалізації/десеріалізації з використанням новоствореного API, запустив код…

Ура! Об'єкти коректно зберігалися і завантажувалися, зберігаючи свій тип! Я гарненько все оттестировал, зберігаючи об'єкти різних класів. Працювало! Зовсім не очікував, що відразу все так просто заведеться…

4. Перші проблеми та боротьба з ними

… і я був абсолютно прав. Насправді, в написаному коді була одна небезпечна помилка. В якийсь момент – а саме, коли я додав черговий зареєстрований клас – все поламалося. Деякі класи перестали реєструватися, причому ознака, за якою отваливалась реєстрація класів, я вловити не міг. Рандом якийсь. Я витратив півгодини часу, переходячи з дебага на логування та назад. Конструктор ClassManager для кожного класу викликався. Реєстрації, відповідно, проходили… Але коли справа доходила до створення об'єкта певного класу globalRuntime.createObject(«SomeHellClass»), то виявлялося, що в масиві зареєстрованих менеджерів класів відсутній ClassManager для класу SomeHellClass.

Хвилин тридать мені здавалося, що хтось із нас двох зійшов з розуму: або я, або С++. І, як завжди, з'ясувалося, що з розуму зійшов все-таки я. Все стало на свої місця, коли я спробував додавати/видаляти вихідні матеріали для передачі їх на компіляцію. Кожен раз при цьому змінювався набір класів, реєстрація яких «отваливалась». Тобто справа була в порядку компіляції исподников.

Люди, які уважно читали код, думаю, вже зрозуміли в чому причина помилки.

Зверніть увагу на те, як був визначений globalRuntime:

//--- CPPRuntime.h ---
extern CPPRuntime globalRuntime;

//--- CPPRuntime.cpp ---
CPPRuntime globalRuntime;


Це глобальний об'єкт. Не вкладений в функцію і створюваний таким чином, у момент першого виклику функції, а просто глобальний об'єкт.

При цьому згадаємо реалізацію конструктора шаблонного класу ClassManager, в якому об'єкти спеціалізації ClassManager реєструють самих себе в рамках об'єкта globalRuntime:

//--- ClassManager.h ---
ClassManager(const char *inClassName)
: IClassManager(inClassName)
{
globalRuntime.registerClass(this);
}


І згадаємо, як створюються об'єкти спеціалізацій ClassManager (вони описуються через макроси):

#define CPPRT_DECLARATION(M_ClassName)\
public:\
static ClassManager< M_ClassName > gClassManager;\
virtual IClassManager *getClassManager() { return &gClassManager; }\
protected:\

#define CPPRT_IMPLEMENTATION(M_ClassName)\
ClassManager< M_ClassName > M_ClassName::gClassManager(#M_ClassName);


Об'єкти спеціалізацій ClassManager (наприклад, об'єкт SomeHellClass::gClassManager) теж є глобальними змінними!.. Повинні бути глобальними змінними: адже важливо, щоб для кожного такого об'єкта виконувався конструктор до запуску main(), інакше не буде виконуватися реєстрація таких об'єктів.

А тепер згадаймо: в якому порядку викликаються конструктори глобальних і статичних змінних в С++?.. Так, вірно. У довільному порядку (stackoverflow, цитата з Стандарту там теж є). Що ж з цього випливає?

А з цього випливає можливість того, що конструктор об'єкта globalRuntime може викликатися після того, як зголосилися конструктори будь-яких спеціалізацій ClassManager. Ситуація дуже недобра: в конструкторах об'єктів спеціалізацій ClassManager (об'єкти з іменами gClassManager) може відбуватися звернення до методів об'єкта, який ще не було створено (globalRuntime). Така поведінка могла призвести до якого-небудь свалу, але не приводило, що ще гірше в даному випадку. Типовий undefined behavior.

Не робіть так. Ніколи.

Необов'язково приміткаНайцікавіше, що відбувається доступ до об'єкта globalRuntime звідки-небудь з «звичайного» коду, який корінням коллстека йде в main(), проблеми не було б: стандарт гарантує, що конструктор глобального об'єкта повинен бути викликаний до виклику main().


Фікс проблеми був очевидний: для коректного доступу до об'єкту потрібно було реалізувати одну з варіацій сінглтона Мейерса (більш детальна стаття про синглтонах, там можете знайти і про цей одинак):

extern CPPRuntime globalRuntime;
//--- CPPRuntime.h ---
CPPRuntime &globalRuntime();


CPPRuntime globalRuntime;
//--- CPPRuntime.cpp ---
CPPRuntime &globalRuntime() {
// Так, знаю, такий підхід не дружить з багатопоточністю, але
// це поки що не критично і легко виправляється – адже у нас не
// header only код. Пулреквестните, якщо бажаєте thread safe...
static CPPRuntime sCPPRuntime;
return sCPPRuntime;
}


Доступ через функцію забезпечує гарантоване створення об'єкта класу CPPRuntime в будь-який момент часу, з будь-якого місця в коді.

Після фікса залишалося тільки внести зміну в конструктор спеціалізацій ClassManager, в якому здійснювався доступ до об'єкта класу CPPRuntime:

ClassManager(const char *inClassName)
: IClassManager(inClassName)
{
// Тепер звертаємося до об'єкта CPPRuntime безпечним шляхом,
// через функцію globalRuntime()
globalRuntime().registerClass(this);
}


Я добряче поганяв код з різних ієрархій об'єктів, змінюючи перелік джерел, що відправляються на компіляцію – щоб точно перевірити, що тепер-то вже все добре.

Все дійсно було добре.

Минав час, основна бібліотека жила своїм життям, розвивалась. І ось в якийсь момент знадобився новий функціонал…

5. Генеалогія класів

В якийсь момент виникла необхідність збирати інформацію про спадкоємців класів під час виконання програми. Я не буду вдаватися в деталі, навіщо це знадобилося – про це розповім як-небудь, коли прийде час опублікувати основну бібліотеку. Щоб вести більш предметну розмову, припустимо, що це було потрібно для налагоджувальних цілей: для зручності перегляду інформації про реєстрованих класах для особливо великих проектів.

Рішення з boost (is_base_of) не підійшло по причині його орієнтованості на використання в compile time (про особливості роботи is_base_of і про API цієї структури в документації boost).

І ось воно, знову… Час прототипировать!



API бачилось десь таким:

std::vector< IClassManager * > theChildManagers;

// Збереження у масив об'єктів спеціалізацій ClassManager
// (приведених до загального для них базового типу IClassManager)
// для класів-спадкоємців класу BaseClass.
globalRuntime(IClassManager).getChildren(BaseClass::gClassManager, theChildManagers);

// Та використання отриманих даних – роздруківка імен
// класів-спадкоємців класу BaseClass:
std::cout << "Children of class " << BaseClass::gClassManager.name() << std::endl;
for (size_t theIndex = 0, theSize = theChildManagers.size(); theIndex < theSize; ++theIndex) {
std::cout << theChildManagers[theIndex]->name() << std::endl;
}


Зрозуміло, що маючи подібний API, можна було б друкувати повні дерева успадкування класів, починаючи з будь-якого батьківського класу.

Створивши таким чином орієнтовний вид API для доступу до нових метаданих про спадкування, я почав думати, як реалізувати реєстрацію з мінімальним ускладненням API реєстрації класів.

Я вибрав саме просте рішення: додати у ClassManager масиви для зберігання покажчиків на ClassManager спадкоємців і базових класів. Ось так:

ClassManager.h
class IClassManager {
private:
. . .
// Масиви для зберігання інформації про
// родинних відносинах класу
std::vector< IClassManager *> _parents;
std::vector< IClassManager *> _children;

protected:
// Метод, який додає посилання на ClassManager для
// базових класів
void setParent(IClassManager *inParent) {
_parents.push_back(inParent);
inParent->_children.push_back(this);
}
. . .
};

//-----------------------------------------------------------------
template < typename T_Type>
class ClassManager : public IClassManager {
. . .
public:
// Зміни в конструкторах, думаю,
// не потребують коментарів
ClassManager(const char *inClassName)
: IClassManager(inClassName)
{
globalRuntime().registerClass(this);
}

ClassManager(const char *inClassName,
IClassManager *inParent0)
: IClassManager(inClassName)
{
globalRuntime().registerClass(this);
setParent(inParent0);
}

ClassManager(const char *inClassName,
IClassManager *inParent0,
IClassManager *inParent1)
: IClassManager(inClassName)
{
globalRuntime().registerClass(this);
setParent(inParent0);
setParent(inParent1);
}

// І т. д. для більшої кількості базових класів
. . .
};



Після цього треба було оновити макроси для реєстрації класів з різним числом базових класів:

Macros.h
// 0 parents
#define CPPRT_IMPLEMENTATION_0(M_Class)\
ClassManager< M_Class > M_Class::gClassManager(#M_Class);

// 1 parent
#define CPPRT_IMPLEMENTATION_1(M_Class, M_BaseClass0)\
ClassManager< M_Class > M_Class::gClassManager(#M_Class,\
&M_BaseClass0::gClassManager);

// 2 parents
#define CPPRT_IMPLEMENTATION_2(M_Class, M_BaseClass0, M_BaseClass1)\
ClassManager< M_Class > M_Class::gClassManager(#M_Class,\
&M_BaseClass0::gClassManager,\
&M_BaseClass1::gClassManager);

// І т. д. для більшої кількості базових класів



Тепер залишається додати в CPPRuntime метод, за допомогою якого будемо обходити базові класи для всіх зареєстрованих спеціалізацій ClassManager. Код для обходу напевно знайомий тим, хто мав справу з обробкою дерев:

CPPRuntime.h
class CPPRuntime {
. . .
private:
// Функція, що реалізує рекурсивний обхід
void CPPRuntime:: getClassRegistries_internal(
IClassManager *inClassManager,
std::vector<IClassManager *> &outRegistries)

public:
// Функція, що входить в інтерфейс класу CPPRuntime.
// Просто викликає з себе getClassRegistries_internal(...)
// Так зроблено, щоб не засмічувати користувальницьке API можливими
// деталями реалізації обходу дерева.
void getChildren(IClassManager *inBaseRegistry,
std::vector< IClassManager *> inChildRegistries);
. . .
};
. . .


CPPRuntime.cpp
. . .
void CPPRuntime::getClassRegistries_internal(
IClassManager *inRegistry,
std::vector<IClassManager *> &outRegistries)
{
// Додаємо поточний ClassManager в результуючий список
outRegistries.push_back(inRegistry);

std::vector< IClassManager * > &theChilds = inRegistry->_childs;
for (size_t theIndex = 0, theSize = theChilds.size(); theIndex < theSize;
++theIndex)
{
// Викликаємо цю ж функцію для всіх спадкоємців
// поточного ClassManager
getClassRegistries_internal(theChilds[theIndex], outRegistries);
}
}


void CPPRuntime::getChildren(IClassManager *inBaseRegistry,
std::vector< IClassManager *> &outRegistries)
{
getClassHeirarhieNames_internal(inBaseRegistry, outRegistries);
}
. . .



Даний код не збирався через порушення інкапсуляції (доступ до поля inRegistry->_childs в методі CPPRuntime:: getClassRegistries_internal). Щоб не засмічувати користувальницький інтерфейс класів ClassManager геттером поля _children, я використовував ключове слово friend. Згоден, що рішення незграбне, але зайвий геттер це теж погано:

ClassManager.h
template < typename T_Type >
class ClassManager : public IClassManager {
. . .
// Щоб CPPRuntime мав доступ до _children.
friend class CPPRuntime;
. . .
};



Після додавання friend код зібрався. Більше того, код заробив — я отримав роздрукований список спадкоємців класу.

Але я тепер був навчений гірким досвідом. На всяк випадок вирішив відразу потестить, додаючи/видаляючи файли в рамках проекту. І знову виліз косяк: часто-густо інформація про спадкування не заповнювалася… Причому масиви _parents заповнювалися правильно, а от _children – ні. Здогадалися в чому проблема?

Проблема все та ж: невизначений порядок виклику конструкторів глобальних змінних. Вся ініціалізація інформації про класі відбувалася в конструкторі ClassManager:

Macros.h
#define CPPRT_IMPLEMENTATION_NO_PREFIX_1(M_Class, M_BaseClass0)\
ClassManager< M_Class > M_Class::gClassManager(\
#M_Class,\
&M_BaseClass0::gClassManager);



В даному коді передається адреса об'єкта спеціалізації ClassManager батьківського класу (&M_BaseClass0::gClassManager) об'єкт спеціалізації ClassManager реєстрованого класу (M_Class::gClassManager), після чого в конструкторі викликається метод setParent(...):

ClassManager.h
class IClassManager {
. . .
void setParent(IClassManager *inParent) {
_parents.push_back(inParent);
inParent->_children.push_back(this); // <<<--- ОСЬ ЦЯ СТРОЧКА!
}
. . .
};



Зазначена в коді рядок звертається до поля ClassManager батьківського класу… Але конструктор об'єкта спеціалізації ClassManager батьківського класу може бути ще не викликаний, адже обидва цих об'єкта на рівних правах статичних об'єктів класів!

Знову маємо звернення до поля ще не створеного об'єкта.

Я сів за пошук вирішення проблеми, що склалася. І ось тут виникла дилема. Справа в тому, що на відміну від сінглтона globalRuntime(), створення об'єктів спеціалізацій ClassManager має відбуватися гарантовано для кожного класу, а не за запитом. Створення цих об'єктів несе функціональне навантаження: в рамках конструкторів спеціалізацій ClassManager виконується логіка реєстрації класів і заповнення метаінформації про класах. Якщо ця логіка не буде виконана в рамках gClassManager для якогось класу SomeHellClass, метаінформація для про спадкування для класів, базових для SomeHellClass, виявиться не заповненою.

ДокладнішеДавайте розглянемо, що буде, якщо організувати «сінглтон за запитом» для спеціалізацій ClassManager, на зразок того, як це зроблено для об'єкта класу CPPRuntime. Код напишемо, «розкриваючи» макроси і опускаючи методи для поліморфного доступу до об'єктів спеціалізацій ClassManager (реалізації віртуальних методів virtual IClassManager *getRegistry()):

//----- Declaration.h -----
class Base {
public:
static ClassManager<Base> *getClassManager( );
};

class Child {
public:
static ClassManager<Child> *getClassManager( );
};

class ChildOfChild {
public:
static ClassManager<ChildOfChild> *getClassManager( );
};


//----- Declaration.cpp -----
ClassManager<Base> *Base::getClassManager() {
static ClassManager<Base> gClassManager("Base");
return &gClassManager;
}

ClassManager<Child> *Child::getClassManager() {
static ClassManager<Child> gClassManager("Child",
Base::getClassManager());
return &gClassManager;
}

ClassManager<ChildOfChild> *ChildOfChild::getClassManager() {
static ClassManager<ChildOfChild> gClassManager("ChildOfChild",
Child::getClassManager());
return &gClassManager;
}


А тепер давайте глянемо, що буде, якщо ми захочемо пройтися по спадкоємцям, наприклад, класу Child. Для цього, принаймні, доведеться отримати доступ до ClassManager для даного класу. Ось так:

ClassManager<Child> *theManager = Child::getClassManager();


При виклику getClassManager() буде виконуватися наступний код:

. . .
static ClassManager<Child> gClassManager("Child", Base::getClassManager()); // <<---!!!
return &gClassManager;
. . .


Тут буде створений об'єкт, в який через виклик Base::getClassManager() буде передана інформація про батьківському класі… АЛЕ інформація про спадкоємців при цьому не буде отримана! Вона буде отримана тільки при виклику ChildOfChild::getClassManager(). Щоб остаточно прояснити ситуацію, згадаймо, як реєструється інформація про спадкування:

// Конструктор спеціалізації ClassManager
. . .
globalRuntime().registerClass(this);
IClassManager::setParent(inParent0); // <<<--- !!!
. . .


Ось. Рядок, з-за якої виникає проблема, виділена знаками оклику. Реєстрація інформації про спадкоємців відбувається під час реєстрації спеціалізації ClassManager для класу-спадкоємця. Якщо getClassManager() для спадкоємця не буде викликаний, в масив _child для батьківського класу не потрапить покажчик на об'єкт спеціалізації ClassManager для спадкоємця.

І головна проблема в тому, що по-іншому реєструвати інформацію про спадкування неможливо – це буде суперечити принципам, закладеним у механізмі наслідування самого С++, а значить, буде громіздко і незручно у використанні.


Таким чином, з одного боку, під час инициализаций ClassManager має бути гарантовано, що конструктор для батьків вже виконався, і гарантувати це можна тільки якщо організувати доступ до об'єкта спеціалізації ClassManager через виклик функції, як це робиться для об'єкта CPPRuntime.

З іншого ж боку, для кожного класу ініціалізація повинна бути гарантована, значить, для кожного класу має бути гарантовано викликана функція для доступу до об'єкта спеціалізації ClassManager. А викликати метод або функцію поза коллстека функції main() дозволяється, тільки якщо її виконання не входить у dynamic initialization для глобальних змінних. Тому потрібно, щоб результат виконання функції чого-небудь присвоювався.

Перший фікс проблеми був зроблений, виходячи з наступного принципу: незважаючи на те, що об'єкти не ініціалізується, вони все ж існують і, отже, мають адресами. Відповідно, в момент реєстрації можна зберігати покажчики в деякому масиві, навіть коли конструктор ClassManager не був викликаний – а для збереження інформації про відношення між класами використовувати індекси покажчиків у рамках цього масиву:

ClassManager.h
class IClassManager {
. . .
std::vector < int > _parentsIndexes;
std::vector < int > _childIndexes;
. . .
void setParent(IClassManager *inParent) {
_parentIndexes.push_back(globalRuntime().indexOf(inParent));
inParent->_childIndexes.push_back(globalRuntime().indexOf(this));
}
. . .
};



CPPRuntime.h
class CPPRuntime {
. . .
std::vector< IClassManager * > _registries;
. . .
int indexOf(IClassManager *inRegistry) {
for(int theIndex = 0, theSize = _registries.size();
theIndex < theSize; ++theIndex)
if (_registries[theIndex] == inRegistry) return theIndex;

_registries.push_back(inRegistry);
return _registries.size() – 1;
}
. . .
};



Рішення було негарним і створювало ще один рівень непрямої адресації, через індекси, що знижувало ефективність коду, причому не тільки під час реєстрації класів, але при будь-якому запиті метаінформації (наприклад, при запиті родичів класів).

Ще один варіант – теж не особливо гарний – використовував обгортку-одинак для gClassManager. Наводжу тут макрос, який використовувався, поки була така реалізація:

Macros.h
. . .
// 1 parent
#define CPPRT_IMPLEMENTATION_1(M_Class, M_BaseClass0)\
ClassManager< M_Class > *M_Class::getRegistry() {
static ClassManager< M_Class > gClassManager(#M_Class,
&M_BaseClass0:: getRegistry() );
return &gClassManager;
}
// Ця конструкція гарантує, що хоча б раз пройде конструктор
// об'єкта для спеціалізації ClassManager< M_Class >, а значить, клас
// буде зареєстрований. Ім'я змінної робимо таким, щоб не було
// проблем з повторним використанням макросів в одному cpp-файлі.
// Зрозуміло, що змінна при цьому лежала мертвим вантажем протягом всієї
// роботи програми. Не особливо великі втрати пам'яті – але брудно,
// брудно!
char __dummy__##M_Class = (char)M_Class::getRegistry();
. . .



І вже під час роботи над цією статтею знайшлося більш витончене рішення. Допоміг один з нюансів пункту 3.6.2 Стандарту. Ось посилання: улюблений мною stackoverflow, через який знайшовся цей пункт і на стандарт (на жаль, не знаю, як дати посилання на розділ в PDF, самі знайдіть пункт 3.6.2 по змісту).

Variables with static storage duration (3.7.1) or thread storage duration (3.7.2) shall be zero-initialized (8.5) before any other initialization takes place


Це означає, що ми можемо застосувати таку позначення (псевдокод):

getPointer() {
// Перевірка коректна, адже до виклику функції, завдяки
// статичної zero-value ініціалізації, покажчик
// гарантовано буде мати значення, що дорівнює нулю
if (!Class::gPointer) { Class::gPointer = initializeValue(); }
return Class::gPointer;
}
Class::gPointer = getPointer();


Я використав цей підхід, після чого провів повну ревізію бібліотечного коду. При цьому сімейство класів ClassManager переїхало жити як внутрішніх класів у CPPRuntime, а сам CPPRuntime отримав більш широкі повноваження.

У результаті зазначених змін вдалося стиснути код бібліотеки до розміру пари файлів сумарним об'ємом 450 рядків коду.

6. Висновок

Залишаю тут посилання на репозиторії бібліотеки:
1. Посилання на «чорнової» репозиторій bitbucket. Так виглядав проект до переїзду на GitHub. За мотивами історії цього репозиторію була написана ця стаття.
2. Посилання репозиторій GitHub — такий, яким він вийшов після перекладу складання проекту на CMake (про що розповідається в статті).

Власне, все. На цьому стаття кінчається. Спасибі за увагу!

Автор заздалегідь дякує читачам за вказівки на помилки в статті, конструктивні поради та побажання

ТитриРедагування статті: Сергій Семенякин


Джерело: Хабрахабр
  • avatar
  • 0

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.