Движок с нуля
Часть 1 · Старт · глава 1.7

C++20 и предупреждения

Включишь в проекте C++20 и строгие предупреждения; на примере std::format увидишь, что бывает, когда версия языка слишком старая.

С этой главы проект открываем и собираем в Visual Studio. Если Visual Studio закрыта, запусти её и открой папку проекта: FileФайл → OpenОткрыть → FolderПапка → D:\dev\MyEngine. В списке конфигураций в верхней полосе окна, слева от кнопки запуска без отладки, должно стоять Debug: эту конфигурацию Visual Studio взяла из пресета сборки debug.

Зачем тогда CMake, пресеты и консоль, если можно было создать проект в Visual Studio

Проект, который создаёт мастер Visual Studio, описан файлами .slnx и .vcxproj, и такие файлы понимает в основном Visual Studio. Наш проект описан в CMakeLists.txt и CMakePresets.json, а Visual Studio просто открывает эти файлы. Что это даёт:

  • Проще подключать библиотеки. Почти все библиотеки C++ собираются через CMake, и такая библиотека подключается к проекту парой строк в CMakeLists.txt. В проекте Visual Studio пути к заголовкам и файлам .lib пришлось бы вписывать вручную в свойства проекта, отдельно для Debug и Release.
  • Проект не привязан к Visual Studio. Тот же CMakeLists.txt откроют другие редакторы, например CLion и VS Code, и соберут другие компиляторы на Linux и macOS.
  • Проект собирается без человека. Можно настроить сервер, который сам собирает проект после каждого изменения. Запустить программу сервер не может, зато выполнит те же команды, что ты набирал в консоли, и возьмёт настройки из того же CMakePresets.json.

А консоль в первых главах показала, что Visual Studio делает за тебя: настраивает проект, запускает компилятор и линкер. Когда сборка в Visual Studio сломается, ты будешь знать, на каком шаге искать причину.

std::format

В этой главе проект получает две настройки: версию языка C++20 и строгие предупреждения. Чтобы увидеть, зачем нужна версия языка, возьмём функцию std::format: она есть только в C++20, и сборка по умолчанию на ней ломается. Движку функция тоже пригодится: через неё программа будет сообщать значения, например размер окна или время кадра.

Обычно текст со значениями печатают так: std::cout << "Window " << width << "x" << height << "\n";. Строка дробится на куски. В std::format строку пишут целиком, а на местах значений ставят {}, значения идут следом:

std::format(“Window {}x{}\n”, width, height)

Первая пара скобок получает значение width, вторая пара — значение height. При ширине 1280 и высоте 720 программа напечатает Window 1280x720.

После двоеточия в скобках задают вид значения. Запись {:.2f} оставляет две цифры после запятой: 3.14159 превращается в 3.14. Так удобно печатать время кадра.

Сборка не пройдёт. На вкладке Error ListСписок ошибок будет предупреждение STL4038: The contents of <format> are available only with C++20 or later., и ошибки C2039, C3861 и C2593 в строке 6 файла main.cpp. Ошибки C2039 и C3861 говорят, что в std нет имени format, а C2593 появляется следом: без std::format компилятор не может разобрать строку печати. Предупреждение называет причину: std::format есть только в C++20 и новее.

У каждой версии C++ свой стандарт языка, и в CMakeLists.txt версия пока не указана. Тогда компилятор MSVC берёт свою версию по умолчанию — C++14.

Предупреждение указывает не на main.cpp, а на строку 42 файла format. Это строка в заголовочном файле, который #include <format> вставил в программу: она проверяет версию языка и сообщает, что для std::format версия слишком старая.

Читай сообщения сборки в порядке появления. В окне OutputВывод сообщения идут в том порядке, в каком компилятор их написал, и первым стоит предупреждение STL4038: оно называет причину. Ошибки C2039, C3861 и C2593 в main.cpp, которые идут ниже, — следствие.

C++20 в CMakeLists.txt

Сборка сломалась не из-за кода в main.cpp: код написан верно, а компилятор собирает по старому стандарту C++14. Версию языка записывают не в коде, а в настройках проекта, то есть в CMakeLists.txt. Сейчас мы напишем там, что программе app нужен C++20, и CMake сам передаст компилятору нужный флаг.

Если не получилось3
Cannot specify compile features for target "ap" which is not built by this project. Вместо ap в кавычках будет слово, которое ты написал в скобках строки target_compile_features первым. Сообщение видно в окне Output.
Почему
CMake не нашёл цель с таким именем. Имя в target_compile_features должно совпадать с первым словом в add_executable буква в букву. Та же ошибка будет, если строка стоит выше add_executable: CMake читает файл сверху вниз, и цель там ещё не создана.
Что сделать

Исправь имя на app, поставь строку ниже add_executable(app main.cpp) и сохрани файл.

target_compile_features specified unknown feature "cxx_std20" for target "app".
Почему
CMake не знает такой возможности компилятора: в слове cxx_std20 пропущен знак подчёркивания.
Что сделать

Напиши cxx_std_20 с двумя знаками подчёркивания и сохрани файл.

C++20 включён, а ошибки C2039 и C3861 про format остались.
Почему
Если на вкладке Error ListСписок ошибок нет предупреждения STL4038, то версия языка уже верная, и причина другая: в main.cpp нет строки #include <format>. Без этой строки компилятор не знает std::format при любой версии языка. Сообщения те же, что и при C++14, поэтому причину отличает только предупреждение STL4038.
Что сделать

Добавь первой строкой main.cpp #include <format>, сохрани файл и собери проект.

Строгие предупреждения

Предупреждение сообщает о месте в коде, которое компилятор переводит, но считает подозрительным. По умолчанию MSVC собирает с уровнем предупреждений /W1: это второй снизу из пяти уровней, от /W0 до /W4, и на нём компилятор о многом молчит. Для движка включим уровень /W4: чем раньше компилятор заметит подозрительное место, тем меньше времени уйдёт на поиск ошибки.

Здесь нужна другая команда CMake, не та, что включала C++20. Команда target_compile_features, которой ты включил C++20, говорила, что нужно программе: «C++20». Какой флаг для этого передать, CMake решал сам для каждого компилятора. Для уровня предупреждений в CMake нет слова вроде cxx_std_20, поэтому команда target_compile_options передаёт компилятору сам флаг, который ты пишешь своими руками.

Две подробности про предупреждения:

  • Предупреждение показывается только после сборки, в которой файл компилировался. Если бы ты собрал проект ещё раз, не удаляя переменную, main.cpp не перекомпилировался бы, и предупреждение пропало бы с вкладки Error ListСписок ошибок, хотя переменная осталась в коде.
  • Если переменная нужна, но может остаться неиспользованной, например когда значение переменной смотрят только в отладчике, поставь перед строкой с переменной атрибут [[maybe_unused]]: строка [[maybe_unused]] int unused = 42; собирается без предупреждения.

Итог

Программа печатает Hello, Engine! через std::format, проект собирается по стандарту C++20, а компилятор сообщает о подозрительных местах на уровне /W4. Так выглядят оба файла после главы:

main.cppкоммит 3f85b6a
#include <format>#include <iostream>int main(){    std::cout << std::format("Hello, {}!\n", "Engine");    return 0;}
CMakeLists.txtкоммит 3f85b6a
cmake_minimum_required(VERSION 3.28)project(Engine LANGUAGES CXX)add_executable(app main.cpp)target_compile_features(app PRIVATE cxx_std_20)if(MSVC)    target_compile_options(app PRIVATE /W4)endif()