C++20 давно в ходу и поддерживается компилятором MSVC с версии 16.11, но в этой статье я расскажу не о том, как его использовать, а как с его помощью нам удалось устранить целый класс багов времени выполнения, подвесив специальную проверку во время компиляции. Давайте разберемся с этим подробнее!  Одна из первых вещей, которые нужно предусмотреть в дизайне компилятора – сообщить программисту, что у него в исходном коде есть ошибка, либо предупредить программиста, что его код может действовать не так, как ожидается. В MSVC инфраструктура ошибки выглядит примерно так:  Ошибка error работает так: у каждого ErrorNumber есть соответствующая строковая запись, и эта строка представляет собой текст, который мы хотим вывести пользователю. Эти текстовые строки могут иметь любой вид от C2056 -> "illegal expression" до C7627 -> "'%1$T': is not a valid template argument for '%2$S'" но что собой представляют эти %1$T и %2$S? Это специальные спецификаторы формата, предусмотренные в компиляторе для отображения определенных типов структур пользователю в удобочитаемом виде.  Спецификаторы формата обеспечивают значительную гибкость в работе разработчику компилятора. Спецификаторы формата позволяют явственнее проиллюстрировать, почему именно было выдано диагностическое сообщение, предоставляют пользователю контекст с более подробной характеристикой проблемы. Загвоздка со спецификаторами форматов в том, что при вызове error они не проходят проверку типов, поэтому, если тип аргумента у нас случайно окажется неправильным, либо мы вообще не передадим аргумент,  то почти наверняка получим ошибку времени выполнения, с которой позже столкнется пользователь. Другие проблемы возникнут, если вы захотите отрефакторить диагностическое сообщение, придав ему более явную форму, но для этого вам потребовалось бы запросить каждого вызывателя данного диагностического сообщения и убедиться, что предложенный рефакторинг согласуется с аргументами, передаваемыми error. При проектировании системы, которая проверяет наши спецификаторы форматов, мы ставим перед собой три наиболее общие цели:  Прямо во время компиляции проверяем типы аргументов, передаваемых нашим диагностическим API, так, что допущенная ошибка выявляется как можно раньше.   Минимизируем изменения, вносимые в вызывателей диагностических API. Это делается, чтобы правильно сформированные вызовы сохраняли свою оригинальную      структуру (и она не разрушалась также у тех вызовов, которые будут делаться в будущем).  Минимизируем изменения, вносимые в детали реализации вызываемой стороны. Мы не должны менять поведение диагностических процедур во время выполнения. Разумеется, есть некоторые решения, введенные в позднейших стандартах C++, при помощи которых также можно было бы попытаться сгладить эту проблему. Во-первых, когда в языке были введены шаблоны с переменным количеством аргументов, появилась возможность попробовать метапрограммирование шаблонов и проверять типы у вызовов к error, но для этого нам понадобилась бы отдельная таблица поиска, поскольку возможности constexpr и шаблонов ограничены. В C++14/17 было введено множество улучшений в constexpr и шаблонные аргументы-константы. Отлично сработало бы нечто такое:  Итак, у нас наконец-то появились инструменты, позволяющие проверять спецификаторы формата во время компиляции. Но проблема не устранена: по-прежнему нет возможности тихо проверить все имеющиеся вызовы к error, и это означает, что нам придется добавить лишний уровень косвенности между точками вызова error, чтобы гарантировать, что ErrorNumber смогла бы выбрать строку во время компиляции и сверить с ним типы аргументов. В C++17 следующий код бы не работал: И мы не смогли бы превратить error как таковой в constexpr, который делает множество вещей, недружелюбных к constexpr. Кроме того, скорректировать все точки вызова в духе error<C2000>(a, b, c), чтобы можно было проверять номер ошибки как выражение времени выполнения – нехорошо, поскольку это спровоцировало бы в компиляторе множество ненужного брожений. В C++20 мы приобрели важный инструмент, обеспечивающий проверку во время компиляции: consteval. consteval – это семейство constexpr, но в их случае язык гарантирует, что функция, снабженная consteval, будет вычисляться во время компиляции. Хорошо известная библиотека под названием fmtlib вводит проверку времени компиляции в рамках core API, причем, это делается без каких-либо изменений в точках вызова – соответственно, предполагается, что форма точки вызова правильно сформирована для работы с библиотекой. Представьте себе упрощенную версию fmt: В данном случае задумано, чтобы format всегда был равен "valid", а T должен представлять собой int. В данном случае код main имеет неправильную форму относительно библиотеки, но не предусмотрено ничего, что проверяло бы этот момент во время компиляции. В fmtlib проверка во время компиляции достигается при помощи небольшого фокуса с типами, определяемыми пользователем: Обратите внимание: необходимо выполнить фокус с std::type_identity_t, чтобы checked не участвовала в выводе типов. Мы хотим только лишь, чтобы она участвовала в выводе остальных аргументов, а выведенные типы этих аргументов будем использовать как шаблонные аргументы для Checker. Можете сами повозиться с этим примером, открыв его в Compiler Explorer. Сила вышеприведенного кода в том, что он служит нам инструментом, позволяющим дополнительно проверить безопасность, не затрагивая ни один из правильно сформулированных вызывателей. При помощи вышеприведенной техники мы применили проверку времени компиляции ко всем нашим процедурам сообщений error, warning и note. Код, использованный в компиляторе, практически идентичен вышеприведенному коду fmt, за исключением того, что аргументом для Checker служит ErrorNumber. Всего нам удалось найти ~120 случаев, в которых либо передается неправильное количество аргументов диагностическому API, либо передается тип, неверный при работе с конкретным спецификатором формата. С годами накопился багаж багов, касающихся странного поведения компилятора при выдаче диагностических сообщений или прямых ICE (внутренних ошибок компилятора) – все дело было в том, что аргументы, искомые спецификаторами форматов, оказывались некорректными или не существовали. При помощи C++20 мы в основном искоренили возможности возникновения таких багов в будущем, но одновременно предоставили возможность без опаски рефакторить диагностические сообщения. Все это было сделано при помощи одного маленького ключевого слова:  consteval.