JavaRush /Курси /C++ SELF /Toolchain: GCC/Clang/MSVC, стандарти та підтримка C++23

Toolchain: GCC/Clang/MSVC, стандарти та підтримка C++23

C++ SELF
Рівень 33 , Лекція 0
Відкрита

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
g++
-std=c++23
-o app
Clang
clang++
-std=c++23
-o app
MSVC
cl.exe
/std:c++20
або
/std:c++latest
(залежно від версії)
/Fe:app.exe
(часто) або через налаштування лінкування

Ця таблиця не для того, щоб ви просто зараз усе запамʼятали. Вона потрібна, щоб зняти психологічний барʼєр: «так, прапорці різні, але завдання одне й те саме».

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, щоб бачити режим мови та сімейство компілятора прямо з вашої програми.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ