В шаблонном коде нужно проверить наличие операции до её использования. Как requires-выражение превращает проверку корректности в логическое условие?
Requires-выражение проверяет, корректны ли указанные типы, выражения или ограничения после подстановки конкретных параметров. Если проверка не проходит, результатом становится false, а не ошибка компиляции в месте проверки; это позволяет использовать результат в ограничении шаблона или в if constexpr.
До C++20 подобные проверки обычно строили через SFINAE, std::void_t, частичные специализации и вспомогательные шаблоны. Такие решения работали, но часто были многословными, плохо выражали намерение и давали диагностику в сложных местах.
В C++20 появились requires-выражения и концепты. Они позволяют описывать проверяемые требования непосредственно в терминах операций над типом, отделяя проверку применимости от реализации шаблона.
Обобщённая функция может быть корректна только для типов, поддерживающих определённую операцию. Если не проверить это заранее, ошибка возникнет внутри тела шаблона и пользователь получит длинное сообщение о недоступном операторе или функции.
Неправильная проверка также может привести к тому, что шаблон будет участвовать в выборе перегрузки для неподходящего типа. Поэтому важно, чтобы условие проверялось во время формирования набора кандидатов, до инстанцирования тела функции.
Внутри requires-выражения записывают последовательность требований. Каждое требование проверяется после подстановки конкретных шаблонных аргументов. Некорректность проверяемого выражения означает false для всего requires-выражения, если ошибка относится именно к проверяемому требованию.
В этом примере требуется, чтобы value.size() был корректным выражением, а его результат приводился к std::size_t. Тип без подходящего size() не делает программу ошибочной сам по себе: функция get_size просто не рассматривается как подходящий кандидат.
Requires-выражение поддерживает несколько видов требований: type requirement проверяет существование типа, compound requirement проверяет выражение и его свойства результата, simple requirement проверяет корректность выражения, а nested requirement проверяет логическое условие.
Важно различать проверку выражения и выполнение выражения. В requires-выражении операции обычно только анализируются компилятором; они не выполняются во время работы программы.
Requires-выражение можно использовать непосредственно как логическое значение, в объявлении концепта или в ограничении шаблона. Ограничение обычно предпочтительнее проверки внутри тела: неподходящий шаблон исключается раньше, а диагностика становится ближе к причине несоответствия.
Однако requires-выражение не превращает любую ошибку программы в false. Ошибка вне проверяемого требования, синтаксически некорректная конструкция или требование, которое не может быть истинным ни для одного возможного типа, может сделать программу некорректной. Кроме того, проверка наличия операции не гарантирует её семантическую правильность: компилятор проверяет формальные свойства, но не смысл результата.
В библиотечном алгоритме требуется получить размер диапазона. Можно было бы предоставить одну универсальную перегрузку и надеяться на ошибку внутри тела, но это ухудшает диагностику и может конфликтовать с другими перегрузками.
Второй вариант — применить std::void_t и частичную специализацию признака. Он совместим со старыми стандартами, но требует вспомогательных типов и хуже показывает само требование.
Выбранное решение — отдельный концепт с requires-выражением. Он явно фиксирует контракт, исключает неподходящие типы до инстанцирования тела и позволяет компилятору сообщить, какое именно требование не выполнено. Компромисс состоит в необходимости C++20 и в том, что концепт проверяет синтаксическую и типовую корректность, но не производительность и не логическую семантику операции.
1. Вопрос: Чем requires-выражение отличается от if constexpr при проверке применимости операции?
Ответ: if constexpr выбирает ветку уже внутри инстанцированной функции. Если неподходящий тип не удовлетворяет условию, альтернативная ветка может быть отброшена, но сама функция уже должна быть успешно рассмотрена как шаблонный кандидат. Requires-выражение может ограничить шаблон до его выбора, поэтому оно влияет на перегрузку и диагностику.
2. Вопрос: Почему проверка наличия метода через requires-выражение не доказывает корректность его поведения?
Ответ: Компилятор проверяет, что выражение допустимо по синтаксису и типам: метод существует, вызывается с указанными аргументами, а результат удовлетворяет заданному ограничению. Он не проверяет, что метод возвращает фактически правильное значение, не изменяет объект неожиданным образом или работает за требуемое время. Такие свойства должны обеспечиваться контрактом библиотеки, тестами или дополнительной статической структурой типов.
3. Вопрос: Что произойдёт, если проверяемое выражение требует вложенного зависимого типа, которого у конкретного типа нет?
Ответ: Если отсутствие типа обнаружено внутри requires-требования, это требование считается невыполненным, а результат проверки становится false. Шаблон с таким ограничением обычно исключается из набора кандидатов без ошибки в месте вызова. Но если само требование сформулировано некорректно для любого возможного шаблонного аргумента, проблема уже не является обычным неуспешным ограничением и может привести к ошибке программы.