Мої перші враження від C++

20 хв. читання

Я займаюсь професійною Веб розробкою вже близько п'ятнадцяти років, переважно у сфері back-end. Починаючи свій шлях в програмуванні, мав іншу вищу освіту і в мене не було змоги попрацювати та оцінити C++ в рамках класичної навчальної програми. Тому цей огляд являє собою точку зору аматора, для якого хоббі стало професією; хто вивчав програмування самостійно і починав це вивчення з мови високого рівня а не навпаки (у моєму випадку - PHP)

Передмова

Так сталось, що у сферу Веб потратив через особистий інтерес до її потенціалу на ті часи, а домінантною мовою в мережі - була PHP, тому і обрав саме її. Так і вивчав: спочатку різні CMS, потім CMF, почав створювати власні самописи, потім бібліотеки.

Звісно, в процесі довелось працювати і з суміжними мовами та їх технологіями (HTML, CSS, JS), так і альтернативи back-end, такі як Python. Оскільки вже більше 10 років користуюсь Linux, час від часу вносив невеличкі контрибуції в проекти на C/C++, відповідно працював з інструментами компіляції, встановлюючи останні версії таких програм. Також в мене був деякий досвід програмування контролерів Arduino, наприклад обробка сигналів різноманітних датчиків, взаємодія з GSM та інше.

Отже заочно з мовами групи C знайомий, оскільки постійно натикався в роботі. Але мені ніколи не доводилось писати на ній щось нове, наприклад з використанням графічного інтерфейсу GTK або для роботи з мережевими інструментами. Одного разу, я знайшов собі таку задачу на вільний час - браузер для протоколу Gemini. Спочатку, написав прототип на вже знайомій мові PHP, з використанням бібліотек PHP-CPP та PHP-GTK3, але виявив що в останній - багато методів GTK не реалізовані, тому витрачав багато часу на доробку бібліотеки, а не створення самої програми. До того ж на момент роботи, актуальною версією була GTK 4 і тут потрібно було або писати бібліотеку з нуля, або створити для браузера окрему гілку і писати його тією мовою, якою написаний сам фреймворк. Власне так і почався мій фундаментальний квест у світ C++

Перехід з PHP

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

Утім, присутня й кардинальна відмінність у моделі роботи з пам'яттю, яка може змусити переписати з нуля майже всю програму.

Вказівники і посилання

Деякі складнощі можуть виникнути з "новими" типами даних: зокрема вказівниками та посиланнями. Це швидко стає зрозумілим, як тільки отримаєте segmentation fault і почнете копати причину вже на реально написаній програмі а не теорії.

Оскільки C++ не використовує рантайм, як PHP, Python чи Go - написати програму без прямого звернення до пам'яті ви просто не зможете, а отже - доведеться трохи розібратись з тим, як для кожного з типів даних програма буде виділяти пам'ять та звільняти її.

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

Від себе тільки додам, що вказівник та посилання - це по суті "ярлик" на певну частину пам'яті, а не копія його об'єкту. Оскільки ваша програма не створює копії об'єктів кожного разу при їх зверненні, це і робить C/C++ таким швидким. З іншого боку, доступ до спільних ресурсів знижує стабільність бо є ризик перезаписати одні і ті само дані в різних частинах програми. Цим і займаються "розумні вказівники" тож по суті - все просто!

Конструкції if та switch

Стосовно конструкцій if - в C++ всі вони повинні оперувати з типами true / false. В принципі, те само й для інших мов, просто там корекція типу відбувається автоматично, а тут неправильний тип викличе помилку компіляції. Я завжди намагаюсь оголошувати типи даних в PHP, тому в C++ це нарешті для мене велика перевага, а ніж незручність.

Також є певні відмінності при роботі зі switch - тут ви не можете просто закинути в умову будь що, а повинні створити або число або попередньо оголошений enum (також є числом). Не зважаючи на те, що С++ синтаксично схожа на PHP, все таки це "найбільш низькорівнева мова високорівневих" (с)

Робота з типами даних

На від міну від C та їй подібних мов, C++ має розширений інструментарій для роботи з висхідними і низхідними операціями перетворення даних: const_cast, static_cast, dynamic_cast, reinterpret_cast. На перший погляд, додаткові конструкції заплутують на етапі вивчення та ускладнюють читабельність коду - потім, але на практиці, дозволяють регулювати співвідношення продуктивності та стабільності програми. Характерні для C перетворення типу (MyClass*) не бажані, оскільки є найменш стабільними, у чому особисто я переконався доволі швидко :)

Перезавантаження функцій

Якось рефакторив код і випадково помітив, що у класі лишилось два однойменні методи, при чому а ні аналізатор, а ні компілятор - не видавали помилок:

void MyClass::method1();
void MyClass::method1(int x, int y);

Наткнувся випадково і швиденько видалив, щоб не соромитись. Але згодом зрозумів, що це є фіча, якої мені давно не вистачало в інших мовах! Таким чином, можна створити багато методів з різним API, а не оголошувати значення атрибутів за замовчуванням та не придумувати якісь ініціальні дані для обов'язкових атрибутів. Тут є своя специфіка, але в цілому - дуже зручна опція!

Рядкові типи даних

Де я спочатку застряг - так це на рядках. По-перше їх можна оголосити (і зберігати у пам'яті) різними способами:

char str[] = "C++";
char str[4] = {'C','+','+','\0'};
const char* str = "C++";
std::string str = "C++";
// ...

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

На щастя, в C++ багато попередньо реалізованих класів, що полегшують таку роботу. Зокрема, в своїй програмі на GTK, я оголошую рядки як об'єкти класів Glib::ustring (для STL є схожий метод) та працюю з ними так само, як в PHP:

using Glib::ustring; // namespace для виклику sprintf

sprintf(
    "%s and %d",
    "substring",
    1
);

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

Масиви

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

Це непорозуміння може виникнути тому, що PHP являє собою препроцесор, по суті заголовковий файл, де вся робота з динамічними даними виконується на етапі інтерпретації до запуску програми середовищем (runtime) якого в C++ просто немає. Тому таку роботу повинен виконати програміст - або на рівні препроцесору або виділяти пам'ять безпосередньо під час роботи програми.

Коли стомитесь від ручного керування, дуже скоро віднайдете для себе "вектори" стандартної бібліотеки, які по суті являють собою допоміжні класи для роботи з динамічними масивами, так само як і відповідні класи для рядків - автоматизують рутинні задачі і роблять це безпечно (хоч і за рахунок зменшення швидкодії):

std::vector<int> numbers;

numbers.push_back(10); // додати 10 в кінець масиву

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

Класи

Якщо в PHP класи - це такі собі структурні одиниці, де описані всі їх компоненти то в C++ класи розділені на файли для компіляції (.cpp) та окремі заголовкові файли (.h, .hpp), що описують структуру та забезпечують попередню логіку препроцесора. По суті, функціональність програми ділиться на таку, що виконується препроцесором до компіляції і ту, яка виконується після такої.

Простори імен

Починаючи зі стандарту C++17, працювати з класами у заголовкових файлах доволі інтуїтивно: можна використовувати як інлайн, так і вкладені конструкції, скорочуючи об'єм коду через using:

namespace myNamespace
{
    class myClass;
}

using myNamespace::myClass;

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

void myClass::methodOne() {}
void myClass::methodTwo() {}
// ...

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

Складається враження, що сучасний підхід ООП з його практиками (зокрема, 1 клас - 1 файл) відбувся вже після того, як сформувалась мова C++, тут відчувається ретро.

Ключове слово this

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

class MyClass
{
    int argument;

    void Method(int argument);
};

void MyClass::Method(int argument)
{
    argument = argument; // :)
}

Звісно, ви можете зробити так:

void MyClass::Method(int argument)
{
    this->argument = argument;
}

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

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

void MyClass::Method(const int & ARGUMENT)
{
    argument = ARGUMENT;
}

Інкапсуляція

Так як фішкою C++ є саме ООП, зі всіма перевагами використання класів - зокрема інкапсуляції, не так давно я стикнувся з неприємним нюансом при роботі з посиланнями, раз вже згадав про них у попередньому розділі.

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

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

Таким чином, посилання або вказівники на дані приватних структур, які я віддаю на зовні, відкривають їх, і мені варто одразу зробити такі структури публічними, що по суті робить їх не класом а struct.

Заголовкові файли

Для того, хто ніколи не зустрічав таких файлів (.h, .hpp) у сучасних мовах вищого рівня, постане логічне питання: навіщо вони взагалі потрібні. Наскільки мені вдалось усвідомити для себе, оскільки в C++ все повинне мати оголошений тип (для відповідного виділення пам'яті) це свого роду мета-інформація для зовнішнього об'єкту, без підключення безпосередньо його логіки (.cpp).

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

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

// оголошення типів даних без підключення Class1, Class2, Class3 через include
class Class1;
class Class2;
class Class3;

void someMethod(Class1, Class2, Class3); // це не викличе помилки компіляції

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

void someMethod(int arg1 = 1, bool arg2 = true);

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

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

Додам ще кілька слів стосовно мого вибору назви розширення для заголовків. Адже можна назвати файл .h, а можна .hpp. Особисто я користуюсь в проекті C++ саме .hpp тому що розробники, які читатимуть код потім, можуть сприйняти .h як сумісний формат також для компіляції C. Це моя суто теоретична логіка.

Багатофайлові проекти

Типовий для PHP include має аналогічний спосіб підключення файлів в C++ через #include який по суті копіює код донора в цільовий файл.

Якщо потрібно використати include_once, то в C для цього використовується директива #pragma once. Можна також користуватись перевіркою визначення констант через #ifdef.

  • include C++ є аналогом require в PHP, оскільки виконується на етапі передкомпіляції

Оскільки в PHP я користуюсь пакетним менеджером та засобами autoload, мені здалось трішки не зручним роботи це знову вручну. Більше того, потрібно в ідеалі спроектувати програму так, щоб не користуватись #pragma once взагалі, оскільки це зайва робота для процесору на етапі збірки, яка виконується постійно.

Пакетний менеджер

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

В принципі, такі пакетні менеджери для C++ є, наприклад Conan. Але мене трохи засмучує, що немає якось єдиного хабу по типу Composer/Packagist для PHP. В цьому плані, звісно дуже виграє Rust з його пакетним менеджером Cargo. Хотілось би подібний набір для C++ з коробки, але все таки це мова, яка сформована спільнотою і має трохи іншу, свого роду анархічну парадигму в своїй основі, яка не прив'язує вас до конкретного рішення і дає повну свободу дій.

Коли дійдуть руки до організації сторонніх бібліотек, в першу чергу, хочу спробувати варіант з git submodule. Це не зовсім пакетний менеджер, але у даного рішення є таке поняття як "теги", з яких формуються безпосередньо версії, git також перед-встановлено на більшості систем тих, хто займається збіркою і не доведеться вимагати від користувача додаткових рухів. Тому наразі це рішення для мене виглядає найбільш привабливим, перш за все - з точки зору універсальності.

Автоматична збірка

До цього поки що не дійшов, але історія обіцяє бути веселою. Не кажучи про мобільні платформи, наразі я розробляю та запускаю програму на Debian 12, але досі не можу зробити цього на Ubuntu 24.04 через конфлікти залежностей, жорстко прописані у Makefile. Окрім make існує чимало альтернативних рішень, але використовуючи їх для себе, мені також доведеться подбати і про класичний спосіб в README.

Вже заздрю користувачам більш сучасних екосистем Rust та Electron :)

IDE

Однією з помилок, якої припускався, коли працював з C++ до того як взявся за справу фундаментально, була відсутність правильно налаштованого середовища для розробки. Раніше я користувався продуктами JetBrains, але потім перейшов на VSCodium, де середовище під кожну мову потрібно збирати самому.

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

Аналізатор коду

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

Особисто я користуюсь плагіном clangd і навіть не уявляю, як раніше редагував і збирав програми без його функцій! Важливо не тільки встановити плагін, але й правильно налаштувати залежності проекту (у даному плагіні це clangd.fallbackFlags), які використовуються в лінкері - без них, встановлений аналізатор просто не розкриє свій потенціал.

Дебагер

Іншим рішенням, без якого не обійтись - це звичайно дебагер для відлагодження коду, у моєму випадку - codelldb

Авто-доповнення

Є думки поставити плагін ШІ, наприклад Copilot, але поки що надаю перевагу Chat GPT і Claude 3 Haiku в браузері. Якщо ви користуєтесь перевіреними плагінами ШІ для C++ то будь ласка, поділіться ними в коментарях - для мене це актуально!

Висновки

Безумовно, C++ є дуже потужним інструментом, що відкриває можливості, які ви не отримаєте від подібних мов на рантаймі. З іншого боку, не завжди такі можливості можуть бути виправдані по часовим витратам, які потрібні на забезпечення їх стабільності.

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

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

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

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

c++
Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
p.s. 626
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід