Разбор последствий: почему два логически эквивалентных ограничения шаблонов могут не определить, какой из них более специализирован?
В C++20 упорядочивание ограниченных шаблонов основано не на логической эквивалентности условий, а на отношении subsumption после нормализации ограничений. Если два ограничения независимо сформулированы как одинаковые по смыслу, их атомарные ограничения обычно считаются разными, поэтому компилятор не может доказать, что одно из них сильнее другого. Результатом может стать неоднозначный вызов.
До появления концептов ограничения шаблонов часто выражали через SFINAE, enable_if и вспомогательные traits. Такие конструкции позволяли исключать неподходящие перегрузки, но плохо описывали намерение и затрудняли упорядочивание нескольких допустимых шаблонов.
Концепты в C++20 сделали ограничения частью интерфейса шаблона. Для выбора между ограниченными кандидатами понадобилось формальное правило: компилятор сравнивает не произвольную истинность предикатов, а структуру их нормализованных ограничений.
Рассмотрим два концепта, каждый из которых проверяет наличие одной и той же операции. Человеку очевидно, что условия эквивалентны, но компилятор не обязан доказывать семантическую эквивалентность произвольных выражений.
Если такие ограничения используются у двух перегрузок, обе перегрузки могут удовлетворять вызову. Поскольку ни одна не subsumes другую, выбор не происходит, и программа завершается ошибкой неоднозначности.
Ограничение разбирается в нормализованную форму, состоящую из логических операций над атомарными ограничениями. Атомарное ограничение — это не просто результат вычисления выражения, а конкретная сущность, связанная с исходным выражением и отображением параметров.
При проверке subsumption компилятор сопоставляет атомарные ограничения после подстановки параметров. Два независимо написанных requires-выражения могут проверять идентичную операцию, но происходить из разных выражений, а значит, не считаться одним и тем же атомарным ограничением.
Минимальная иллюстрация:
Оба ограничения истинны для int, но HasPlus и HasPlusAgain содержат разные атомарные ограничения. Поэтому компилятор не видит отношения «вторая перегрузка более специализирована».
Надёжный подход — определить одно каноническое ограничение и строить более сильные ограничения через него. Например, концепт для целочисленного типа, дополнительно использующий HasPlus, сохраняет общую атомарную часть и может быть распознан как более сильный. Простое текстовое повторение одинаковой проверки такой связи не создаёт.
Важно отличать это от проверки истинности условий. При выборе шаблона компилятор сначала проверяет удовлетворённость constraints, а затем упорядочивает подходящие кандидаты. Логическая эквивалентность на уровне математики не заменяет структурное правило subsumption.
В библиотеке есть универсальная перегрузка для типов, поддерживающих форматирование, и специализированная перегрузка для типов, поддерживающих то же форматирование плюс сериализацию. Команда независимо описала обе проверки через два похожих requires-выражения.
Возможные варианты:
enable_if — можно управлять участием перегрузок, но диагностика и правила упорядочивания становятся менее прозрачными;Выбран третий вариант. Общий концепт переиспользуется в составном ограничении, поэтому нормализация сохраняет общие атомарные ограничения, а дополнительное условие делает специализированную перегрузку более constrained. В результате вызов однозначно выбирает нужную реализацию, а диагностика для неподходящих типов остаётся понятной.
1. Достаточно ли того, что два ограничения возвращают одинаковое логическое значение?
Нет. Для subsumption важно происхождение и структура атомарных ограничений, а не только значение true или false для конкретного типа. Компилятор не выполняет общий анализ математической эквивалентности произвольных выражений.
2. Почему объединение условий через логическое «И» может изменить результат упорядочивания?
Нормализация раскрывает составное ограничение и сохраняет его структуру. Если одно ограничение содержит атомарные ограничения другого и добавляет новые через &&, оно может subsume более общее ограничение. Поэтому явная композиция концептов формирует информацию для упорядочивания, которой нет у двух независимо написанных проверок.
3. Можно ли считать два использования одного концепта разными атомарными ограничениями?
Обычно нет: при нормализации концепт раскрывается с учётом исходного атомарного ограничения и отображения параметров. Именно поэтому повторное использование одного именованного концепта обычно позволяет установить связь между ограничениями. Однако изменение структуры выражения, обёртка в независимый requires или отдельное повторное написание проверки может создать другую атомарную сущность.