Як порівнювати структури у с. структури. Повний текст макросів

Мова C++ для всіх класів і структур користувача генерує за замовчуванням копіруючий конструктор і копіюючий оператор присвоювання. Тим самим було важливого ряду випадків програміст звільняється від написання зазначених функцій вручну. Наприклад, за промовчанням оператори добре працюють для структур, які містять дані. При цьому дані можуть зберігатися як у простих типах, так і складних контейнерах, таких як std::vector або std::string.

У світлі цього зручно було б мати і оператори порівняння структур == і!= за умовчанням, проте компілятор C++, відповідно до стандарту, не генерує їх.

Написати оператор почленного порівняння структур нескладно, проте така організація програми є незручною та небезпечною з погляду помилок. Скажімо, якщо програміст додасть новий член у структуру, але забуде додати відповідне порівняння в операторі користувача порівняння - то в програмі утворюється досить важко діагностована помилка. Тим більше що оголошення структури і оператор її порівняння зазвичай рознесені один від одного, так як знаходяться в різних файлах (*.h і *.cpp).

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

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

Інші існуючі рішення

На даний момент мені відомі наступні альтернативні рішення описаної проблеми:

  1. Використання динамічних структур. Замість звичайної структури C++ застосовується контейнер різнорідних елементів, які наведено єдиного типу. Наприклад, типу VARIANT із Windows OLE. Також використовується контейнер рядків для зберігання імен членів. Тим самим імена членів, їх типи та кількість робляться доступними програмі під час виконання. Однак, такий підхід призводить до витрат під час виконання програми на доступ до членів такої структури. Синтаксис доступу виду object.member_name або pObject->member_name стає недоступним, його доводиться змінювати на щось на зразок object.at(“member_name”). Крім того, є лінійне зростання споживання пам'яті: кожен екземпяр структури займає більше місця у пам'яті, ніж звичайна (статична) структура.
  2. Використання бібліотеки boost, а саме контейнера boost::fusion::map. Тут удалося всі витрати звалити на плечі компілятора, проте традиційний синтаксис доступу до членів зберегти не вдалося. Доводиться скористатися конструкціями виду: at_key (object).
  3. Генерація коду C++. Опис структури на C++ та оператора її порівняння не пишеться програмістом вручну, а генерується скриптом на основі опису структури якоюсь іншою вхідною мовою. Даний підхід, на мою думку, є ідеальним, але я на даний момент не реалізував його, тому в статті мова не про нього.
Рішення на базі макросів

Рішення, яке мені вдалося реалізувати за допомогою макросів, має такі переваги:

  • Відсутнє навантаження під час виконання доступу до членів структури.
  • Вдалось зберегти стандартний синтаксис доступу до членів структури виду object.member_name або pObject->member_name.
  • Навантаження на згадку про вид O(1). Іншими словами, кожен екземпляр структури з автопорівнянням займає стільки ж місця у пам'яті, як і звичайна структура. Є лише постійні (невеликі) витрати пам'яті на кожен тип таких структур, що оголошується.

З недоліків можна назвати такі:

  • Наявність у структурі додаткових службових членів, що знижує зручність таких інструментів аналізу, як Intellisense чи DoxyGen.
  • Можливість конфлікту імен службових членів з користувачами.
  • Неможливість ініціалізації списком ініціалізаторів виду struct a = (1,2,3).
Приклад використання

Нехай нам потрібно створити структуру для зберігання даних про людей, еквівалентну наступній звичайній структурі:

Struct MANPARAMS ( std::string name; int age; std::vector friend_names; double karma; );

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

Class AUTO_MANPARAMS ( PARAMSTRUCT_DECLARE_BEGIN(AUTO_MANPARAMS); public: DECLARE_MEMBER_PARAMSTRUCT(std::string, name); DECLARE_MEMBER_PARAMSTRUCT(int, age); DECLARE_MEMBER_PARAMSTRUCT( , friend_names); DECLARE_MEMBER_PARAMSTRUCT(double, karma); );

Після цього один раз на всю програму необхідно скомпілювати наступний виклик макросу в одному з *.cpp-файлів:

PARAMFIELD_IMPL(AUTO_MANPARAMS);

Всі! Тепер можна спокійно користуватися даними структурами як завжди, і порівнювати їх на рівність чи нерівність, не переймаючись написанням відповідних операторів. Наприклад:

Void men(void) (AUTO_MANPARAMS man1, man2; man1.name = "John Smith"; man1.age = 18; man1.karma = 0; man2.name = "John Doe"; man2.age = 36; man2.karma = 1; man2.friends.push_back("Sergud Smith"); if(man1 == man2) printf("Ku-ku!n");

Реалізація

Як видно з вищенаведеного, на початку визначення кожної структури необхідно викликати макрос PARAMSTRUCT_DECLARE_BEGIN(x), який визначить для цієї структури деякі загальні типи та статичні службові члени. Після цього потрібно при оголошенні кожного члена користувача викликати другий макрос, DECLARE_MEMBER_PARAMSTRUCT(type, name), який, крім оголошення власне члена із зазначеним ім'ям, визначає службові члени структури, пов'язані з ним.

Основні ідеї реалізації:

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

Кожна така функція є членом структури і здійснює порівняння «свого» члена даних. Вона генерується в макросі DECLARE_MEMBER_PARAMSTRUCT(type, name) таким чином:

Bool comp##name(const ThisParamFieldClass& a) const ( return name == a.name; )

Де ThisParamFieldClass – це тип нашої структури, який оголошується через typedef в головному макросі - див.

2. Масив із покажчиками на функції порівняння

Головний макрос PARAMSTRUCT_DECLARE_BEGIN(x) повідомляє статичний масив, в якому зберігаються покажчики на кожну з функцій порівняння членів. Для цього спочатку визначається їх тип:

#define PARAMSTRUCT_DECLARE_BEGIN(x) private: typedef x ThisParamFieldClass; typedef bool (ThisParamFieldClass::*ComFun)(const ThisParamFieldClass& a) const; struct MEM_STAT_DATA ( std::string member_name; ComFun comfun; );

А потім оголошується масив:

Static std::vector stat_data;

Тут же оголошуються оператори порівняння:

Public: bool operator==(const ThisParamFieldClass& a) const; bool operator!=(const ThisParamFieldClass& a) const ( return !operator==(a); )

Реалізується оператор порівняння іншим макросом (PARAMFIELD_IMPL), проте його реалізація є тривіальною за наявності заповненого масиву stat_data: потрібно лише викликати функцію порівняння для кожного елемента цього масиву.
Для лише порівняння структур немає необхідності зберігати в масиві імена членів структури. Однак збереження імен дозволяє розширювати концепцію, застосовуючи її не лише до почленного порівняння, але й до інших операцій, наприклад, збереження та завантаження в текстовому форматі, придатному для читання людиною.

3. Заповнення даних про членів структури

Залишається вирішити питання із заповненням масиву stat_data. Оскільки інформація про членів спочатку недоступна ніде, крім макросу DECLARE_MEMBER_PARAMSTRUCT, то заповнювати масив можна лише звідти (прямо чи опосередковано). Однак цей макрос викликається всередині оголошення структури, що не є найзручнішим місцем для ініціалізації std::vector. Я вирішив цю проблему за допомогою службових об'єктів. Для кожного члена структури оголошується службовий клас та об'єкт цього класу. Цей клас має конструктор - він і додає інформацію про елемент у статичний масив stat_data:

Class cl##name ( public: cl##name(void) ( if(populate_statdata) ( MEM_STAT_DATA msd = ( #name, &ThisParamFieldClass::comp##name ); stat_data.push_back(msd); ) ) ); cl##name ob##name;

де populate_statdata – статичний прапор, який оголошується в головному макросі та сигналізує про те, чи слід заповнювати масив stat_data іменами членів структури та функціями їх порівняння. При старті програми механізм ініціалізації, описаний нижче, встановлює populate_statdata=true та створює один екземпляр структури. При цьому конструктори службових об'єктів, пов'язані з кожним членом структури, заповнюють масив даних про членів. Після цього встановлюється populate_statdata=false і статичний масив з інформацією про членів більше не змінюється. Дане рішення призводить до деяких втрат часу при кожному створенні структури програмою користувача, на перевірку прапора populate_statdata. Однак витрата пам'яті не збільшується: службовий об'єкт не містить членів даних, лише конструктор.

І, нарешті, механізм управління прапором populate_statdata: реалізується за допомогою статичного службового об'єкта з конструктором, одного на всю структуру. Цей об'єкт оголошується в головному макросі:

Class VcfInitializer (public: VcfInitializer(void); ); static VcfInitializer vcinit;

Реалізація конструктора знаходиться у макросі PARAMFIELD_IMPL(x):

X::VcfInitializer::VcfInitializer(void) ( x::populate_statdata = true; ThisParamFieldClass dummy; x::populate_statdata = false; )

Повний текст макросів
#define PARAMSTRUCT_DECLARE_BEGIN(x) private: typedef x ThisParamFieldClass; typedef bool (ThisParamFieldClass::*ComFun)(const ThisParamFieldClass& a) const; struct MEM_STAT_DATA ( std::string member_name; ComFun comfun; ); static std::vector stat_data; static bool populate_statdata; public: bool operator==(const ThisParamFieldClass& a) const; bool operator!=(const ThisParamFieldClass& a) const ( return !operator==(a); ) private: class VcfInitializer ( public: VcfInitializer(void); ); static VcfInitializer vcinit; #define DECLARE_MEMBER_PARAMSTRUCT(type,name) public: type name; private: bool comp##name(const ThisParamFieldClass& a) const ( return name == a.name; ) class cl##name ( public: cl##name(void) ( if(populate_statdata) ( MEM_STAT_DATA msd = ( #name , &ThisParamFieldClass::comp##name, );stat_data.push_back(msd); ) ) ); cl##name ob##name; #define PARAMFIELD_IMPL(x) std::vector x::stat_data; bool x::populate_statdata = false; x::VcfInitializer x::vcinit; x::VcfInitializer::VcfInitializer(void) ( x::populate_statdata = true; ThisParamFieldClass dummy; x::populate_statdata = false; ) bool x::operator==(const x& a) const ( bool r = true; for( size_t i = 0;r && i *stat_data[i].comfun)(a); ) return r; )
Висновок

На основі наведених вище макросів можна оголошувати структури, котрим автоматично створюються оператори порівняння та інші почленные операції. До інших таких операцій відносяться, наприклад, завантаження та збереження в текстові файли, формат XML. Відсутність дублювання коду полегшує роботу та оберігає від помилок. Одне лише оголошення члена структури додає цей член до операцій порівняння, збереження та завантаження.

Структури

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

Для вирішення подібних труднощів C# передбачена структура, яка подібна до класу, але відноситься до типу значення, а не до типу посилань. Тобто. структури відрізняються від класів тим, як вони зберігаються в пам'яті і як до них здійснюється доступ (класи - це типи посилань, що розміщуються в купі, структури - типи значень, що розміщуються в стеку), а також деякими властивостями (наприклад, структури не підтримують успадкування) . З міркувань продуктивності ви використовуватимете структури для невеликих типів даних. Однак щодо синтаксису структури дуже схожі на класи.

Головна відмінність полягає в тому, що при їх оголошенні використовується ключове слово structзамість class. Нижче наведено загальну форму оголошення структури:

struct ім'я: інтерфейси (// оголошення членів)

де ім'я означає конкретне ім'я структури.

Як і в класів, кожна структура має свої члени: методи, поля, індексатори, властивості, операторні методи і події. У структурах допускається також визначати конструктори, але з деструктори. У той самий час для структури не можна визначити конструктор, який використовується за умовчанням (тобто конструктор без параметрів). Справа в тому, що конструктор, що викликається за умовчанням, визначається для всіх структур автоматично і не змінюється. Такий конструктор ініціалізує поля структури значеннями, що задаються за умовчанням. Оскільки структури не підтримують успадкування, їх члени не можна вказувати як abstract, virtual чи protected.

Об'єкт структури може бути створений за допомогою оператора newтак само, як і об'єкт класу, але в цьому немає особливої ​​необхідності. Адже коли використовується оператор new, то викликається конструктор, який використовується за умовчанням. А коли цей оператор не використовується, об'єкт, як і раніше, створюється, хоч і не ініціалізується. У цьому випадку ініціалізацію будь-яких членів структури доведеться виконати вручну.

Давайте розглянемо приклад використання структур:

Using System; namespace ConsoleApplication1 ( // Створимо структуру struct UserInfo ( public string Name; public byte Age; public UserInfo(string Name, byte Age) ( this.Name = Name; this.Age = Age; ) public void WriteUserInfo() ( Console.WriteLine ("Ім'я: (0), вік: (1)",Name,Age); ) ) class Program ( static void Main() ( UserInfo user1 = new UserInfo("Alexandr", 26); Console.Write("user1 : "); user1.WriteUserInfo(); UserInfo user2 = new UserInfo("Elena",22); Console.Write("user2: "); user2.Name = "Natalya"; user2.Age = 25; Console.Write("\nuser1: "); user1.WriteUserInfo(); Console.Write("user2: "); ReadLine(); ) ) )

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

Тому, якби в попередньому прикладі використовувався клас UserInfo замість структури, вийшов би такий результат:

Призначення структур

У зв'язку з викладеним вище виникає резонне питання: навіщо в C# включена структура, якщо вона має скромніші можливості, ніж клас? Відповідь на це питання полягає у підвищенні ефективності та продуктивності програм. Структури відносяться до типів значень і тому ними можна оперувати безпосередньо, а не за посиланням. Отже, для роботи зі структурою взагалі не потрібна змінна типу посилань, а це означає в ряді випадків суттєву економію оперативної пам'яті.