1. Навіщо знати toolchain
Коли ви працюєте в IDE, вона схожа на «розумного дворецького»: сама викликає компілятор, сама добирає прапорці, сама десь на тлі запускає лінкер. Через це початківець легко потрапляє в пастку: здається, ніби «магія збирання» вбудована в саму мову. Але щойно ви виходите з IDE або проєкт раптом не збирається в вашого друга, зʼясовується: магія — це лише набір інструментів і налаштувань.
Термін toolchain — це зручна назва для «набору інструментів і ключів», які перетворюють ваші .cpp на програму, яку можна запустити. Якщо ви розумієте, що входить до toolchain, то швидше відрізните «у мене зламаний код» від «у мене інструменти налаштовані інакше» й перестанете лікувати гайки молотком.
Toolchain на пальцях: компіляція, лінкування і std::vector
Toolchain у практичному сенсі — це не лише «компілятор». Це щонайменше три великі частини: компілятор, лінкер і стандартна бібліотека. І ось важливий момент: коли ви кажете «у мене GCC», то часто насправді маєте на увазі цілу звʼязку інструментів — компілятор, лінкер і бібліотеку, які добре працюють разом.
Нижче — схема, яку корисно тримати в голові. Не як екзаменаційний малюнок, а як «мапу місцевості», щоб розуміти, де шукати проблему.
flowchart LR
A["Вихідні файли: .cpp/.hpp"] --> B["Компілятор (compiler)"]
B --> C["Обʼєктні файли: .o/.obj"]
C --> D["Лінкер (linker)"]
D --> E["Виконуваний файл (exe/app)"]
subgraph StdLib["Стандартна бібліотека й середовище виконання"]
S1["Заголовки: <vector>, <string>, ..."]
S2["Бінарна частина: libstdc++/libc++/MS STL"]
end
S1 --> B
S2 --> D
Заголовки стандартної бібліотеки беруть участь у компіляції, бо там містяться оголошення. А бінарна частина стандартної бібліотеки бере участь у лінкуванні, бо там уже є «готові шматки», які треба приєднати до вашої програми. Це одна з причин, чому слово toolchain ширше, ніж compiler.
Сімейства toolchain: GCC, Clang/LLVM і MSVC
Поширена помилка початківця — думати, що «C++23» гарантує однакову поведінку на всіх компіляторах. Стандарт задає правила мови й бібліотеки, але реальний світ складається з реалізацій, і в них бувають різні темпи впровадження можливостей, різні попередження й різні «гострі кути». Це нормально: стандарт один, а реалізацій кілька.
У повсякденній розробці ви найчастіше зустрінете три сімейства:
GCC (GNU Compiler Collection) — історично дуже популярний у Linux-світі набір компіляторів. Для C++ зазвичай використовують драйвер-команду g++.
Clang/LLVM — сучасна інфраструктура компіляції. Clang часто цінують за зручні повідомлення про помилки, якісну діагностику та широкі можливості вбудовування. Для C++ зазвичай використовують clang++.
MSVC (Microsoft Visual C++) — компілятор із Visual Studio. Його «обличчя» в консолі — cl.exe, а лінкер зазвичай — link.exe. У Windows це дуже поширена екосистема.
Важливо: усі троє компілюють C++, але в кожного свої ключі, свої значення деяких макросів і, часом, різний ступінь готовності окремих частин C++23, особливо бібліотечних.
2. Режим мови та як перевірити стандарт
Драйвер-команда g++/clang++ і чому це важливо
Коли ви бачите команду g++ або clang++, можете думати про неї як про «диригента». Вона не обовʼязково виконує все однією внутрішньою програмою. Найчастіше вона запускає кілька стадій: компіляцію, асемблювання й лінкування. Але головне тут інше: драйвер для C++ за замовчуванням правильно підʼєднує C++-середовище, зокрема стандартну бібліотеку C++ на стадії лінкування.
Це одна з причин, чому в C++ зазвичай використовують саме g++/clang++, а не gcc/clang. Формально gcc може компілювати C++-файли, але звичний комфорт з автоматичним підʼєднанням C++-бібліотеки та середовища виконання C++ ви частіше отримаєте саме з g++.
У світі MSVC роль «драйвера» виконує cl.exe, але там історично інший стиль ключів і інший формат середовища, особливо через те, що Windows любить, коли все лежить у конкретних місцях і запускається з «Developer Command Prompt».
Чому C++23 — це прапорець збирання, а не властивість файла
Дуже хочеться вірити, що файл main.cpp «сам по собі» є C++23. На жаль: файл — це просто текст, а те, за якими правилами цей текст інтерпретуватиметься, вирішує компілятор — і робить це через прапорці, тобто налаштування режиму.
Тобто «режим мови» — це частина команди збирання. І це важливо з двох причин.
Перша причина прагматична: різні середовища можуть мати різні стандарти за замовчуванням. Десь це C++14, десь C++17, а десь — «якийсь C++2a, але не питайте». Якщо ви не фіксуєте стандарт, то самі собі створюєте майбутній баг «у мене працює».
Друга причина трохи філософськіша: стандарт — це не лише синтаксис. Це ще й доступність частин стандартної бібліотеки, і набір feature-test-макросів, і, часом, навіть поведінка деяких заголовків. Тому дисципліна проста: завжди явно вказуйте стандарт під час збирання, якщо хочете, щоб результат можна було відтворити.
__cplusplus: як дізнатися, за яким стандартом ви реально зібрали
Серед наперед визначених макросів у C++ є один, який спеціально показує «режим мови»: __cplusplus. Він існує не для краси, а як діагностичний маячок: «яку версію стандарту зараз увімкнено?».
Історично значення __cplusplus змінювалося разом з оновленнями стандарту. Нам це важливо не як «які саме числа», а як факт: це офіційний механізм, який має відображати вибраний стандартний режим.
Міні-демо: друкуємо __cplusplus
Ідея проста: перш ніж сперечатися «у мене точно C++23», можна один раз чесно спитати в компілятора.
#include <iostream>
int main() {
std::cout << "__cplusplus = " << __cplusplus << '\n';
return 0;
}
// приклад виводу (значення залежить від режиму компіляції):
// __cplusplus = 202302
Число у виводі зручно сприймати як «рік + місяць стандарту у форматі YYYYMM», але вам не потрібно цього зубрити — достатньо розуміти принцип: якщо ви перемкнули стандарт, це число має змінитися.
Важлива обмовка про MSVC
У MSVC є історична особливість: значення __cplusplus тривалий час не відображало реальний стандартний режим без додаткового налаштування, зазвичай через прапорець сумісності. Тому у Windows ви можете побачити ситуацію, коли «можливості майже як у C++20», а __cplusplus раптом виглядає як «дуже старий». Це не ви збожеволіли — це старий компроміс сумісності.
Практичний висновок: у MSVC для діагностики режиму часто використовують не лише __cplusplus, а й _MSC_VER плюс правильні прапорці режиму.
5. Підтримка C++23: мова, бібліотека та перевірки
Фраза «мій компілятор підтримує C++23» звучить просто, але насправді вона розпадається на два запитання.
Перше запитання: чи підтримує компілятор можливості мови. Це про синтаксис і семантику: які ключові слова є та які правила діють.
Друге запитання: чи підтримує стандартна бібліотека бібліотечні можливості C++23. Це про те, чи є в <print> потрібні функції, чи реалізовано очікуваний набір алгоритмів тощо.
І ось тут починається найцікавіше: можна натрапити на ситуацію, коли компілятор загалом «уміє мову», але бібліотека ще не містить частини можливостей. Або навпаки: бібліотека вже щось уміє, але конкретне налаштування режиму не вмикає потрібні перевірки чи макроси.
Feature-test-макроси: перевіряємо по-дорослому
У стандарті C++ (і в бібліотеках) є ідея feature-test-макросів: спеціальних __cpp_* і __cpp_lib_*, які дають змогу зрозуміти, чи доступна конкретна можливість.
Ми зараз не влаштовуватимемо енциклопедію макросів, але покажемо базовий практичний прийом: підключити <version> і перевірити, чи оголошено бібліотечний макрос.
#include <iostream>
#include <version>
int main() {
#ifdef __cpp_lib_print
std::cout << "std::print доступний\n"; // std::print доступний
#else
std::cout << "std::print недоступний\n"; // std::print недоступний
#endif
}
Сенс такий: ми не «віримо на слово» прапорцю -std=c++23, а вміємо акуратно перевірити в середовищі: «чи справді ця бібліотечна можливість доступна?». Це особливо корисно, коли ви пишете переносний код або навчаєтеся на різних компʼютерах.
Шпаргалка з командного збирання
Коли ви вперше бачите команди збирання для різних toolchain, здається, що це три різні мови командного рядка. Насправді логіка одна й та сама: задати режим мови, передати вхідні файли й отримати результат. Відрізняються лише «слова», якими ви це говорите.
| Сімейство | Типова C++-команда | Прапорець стандарту | Як зазвичай вказують імʼя результату |
|---|---|---|---|
| GCC | |
|
|
| Clang | |
|
|
| MSVC | |
або (залежно від версії) |
(часто) або через налаштування лінкування |
Ця таблиця не для того, щоб ви просто зараз усе запамʼятали. Вона потрібна, щоб зняти психологічний барʼєр: «так, прапорці різні, але завдання одне й те саме».
6. Практичний приклад: build info в Tasker
Зараз зробимо маленький, але дуже корисний крок у нашому консольному застосунку, який ми поступово розвиваємо. Нехай це буде простий застосунок Tasker (мініпланувальник завдань), який до цього моменту вже складається з кількох .cpp/.hpp і вміє хоча б запускатися та друкувати довідку. Ми додамо функцію, яка виводить інформацію про збирання: який компілятор використано, який режим мови ввімкнено, які ключові макроси доступні.
Це здається «зайвим», доки все працює. Але щойно ви почнете збирати один і той самий проєкт у різних toolchain, build info стане вашим найкращим другом: він чесно скаже, чим одне збирання відрізняється від іншого.
Заголовок build_info.hpp
Ідея проста: у заголовку тримаємо оголошення, щоб main.cpp міг викликати функцію, але не знав деталей.
#pragma once
#include <string>
namespace tasker {
std::string BuildInfo();
}
Реалізація build_info.cpp
Ідея тут така: ми хочемо зібрати рядок із кількох макросів і повернути його. Зверніть увагу: ми не використовуємо нічого «страшнішого» за рядки та #if.
#include "build_info.hpp"
namespace tasker {
std::string BuildInfo() {
#if defined(__clang__)
return "compiler=clang, __cplusplus=" + std::to_string(__cplusplus);
#elif defined(__GNUC__)
return "compiler=gcc, __cplusplus=" + std::to_string(__cplusplus);
#elif defined(_MSC_VER)
return "compiler=msvc, __cplusplus=" + std::to_string(__cplusplus);
#else
return "compiler=unknown, __cplusplus=" + std::to_string(__cplusplus);
#endif
}
} // namespace tasker
Якщо ви раптом подумали «а де #include <string>?», то він уже є в заголовку, а .cpp підтягує його через build_info.hpp. Це якраз одна з причин любити акуратні заголовки: менше дублювання.
Використання в main.cpp
Ідея така: нехай у нас буде аргумент --build-info, який виводить паспорт збирання. Це допоможе вам (і викладачу) миттєво побачити, у якому режимі зібрано застосунок.
#include <iostream>
#include <string>
#include "build_info.hpp"
int main(int argc, char** argv) {
if (argc >= 2 && std::string(argv[1]) == "--build-info") {
std::cout << tasker::BuildInfo() << '\n';
return 0;
}
std::cout << "Tasker: запустіть із --build-info\n"; // Tasker: запустіть із --build-info
}
Так, це поки що не «багатий функціонал планувальника завдань». Але це важлива інженерна звичка: уміти пояснити, що саме являє собою ваш бінарник.
7. Коли toolchain «ламає мозок» початківцю
Перша ситуація виглядає так: «код нормальний, але у друга не компілюється». Часто причина навіть не в коді, а в тому, що у вас різні стандарти за замовчуванням або різні реалізації бібліотек. Якщо ви фіксуєте стандарт прапорцем і вмієте друкувати build info, то різко скорочуєте коло пошуку.
Друга ситуація: «ніби C++23 увімкнено, але потрібного заголовка чи функції немає». Це майже завжди про різницю «мова vs бібліотека». Компілятор міг увімкнути режим мови, але стандартна бібліотека у вашому середовищі ще не реалізувала потрібну частину. Тоді правильний хід — або не використовувати цю можливість, або перевірити її feature-test-макросом і мати запасний варіант.
8. Типові помилки
Помилка № 1: сприймати C++23 як властивість вихідного файла, а не як налаштування збирання.
Якщо ви не фіксуєте стандарт, то рано чи пізно зловите ситуацію «у мене працює, у тебе — ні». І це буде не містичний баг, а просто інший режим компілятора. Звичка явно вказувати стандарт — це не занудство, а страховка від дивних розбіжностей.
Помилка № 2: вважати, що підтримка C++23 означає автоматичну наявність усіх бібліотечних новинок.
Мова й бібліотека розвиваються разом, але впроваджуються не завжди синхронно. У результаті ви можете побачити: режим мови увімкнено, але якась бібліотечна можливість відсутня. Вихід — перевіряти feature-test-макроси й ставитися до бібліотеки як до окремої частини toolchain, а не як до «магії всередині компілятора».
Помилка № 3: намагатися діагностувати режим мови на око, не перевіряючи __cplusplus.
__cplusplus існує саме для того, щоб не вгадувати. Історично його значення оновлювалося разом зі стандартами, і це робить його зручним маркером. Якщо у вас є сумніви, краще один раз вивести __cplusplus, ніж пів години сперечатися з реальністю.
Помилка № 4: у Windows очікувати, що MSVC поводитиметься точно так само, як GCC/Clang, за ключами й макросами.
MSVC — окрема екосистема зі своєю історією сумісності, своїми ключами та своєю поведінкою деяких макросів. Якщо ви переносите команди напряму між GCC і MSVC, то майже гарантовано отримаєте дивні помилки. Правильний підхід — спочатку зрозуміти, який у вас toolchain, і вже потім говорити з ним «його мовою».
Помилка № 5: плутати компілятор і стандартну бібліотеку, кажучи «у мене GCC».
На практиці важливий не лише компілятор, а й те, з якою стандартною бібліотекою він працює (і який лінкер бере участь). Тому «у мене GCC» — корисна, але неповна фраза. Значно корисніше вміти вивести build info, щоб бачити режим мови та сімейство компілятора прямо з вашої програми.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ