В этой статье хотелось бы кратко рассмотреть особенности применения механизма обнаружения наличия функциональности у используемых типов данных на этапе компиляции. В большей степени такое обнаружение необходимо для возможности формирования вразумительных сообщений об ошибках в обобщенном программировании с помощью шаблонов, а также для унификации и оптимизации реализуемых алгоритмов. Например, простейший код при компиляции выдаст такое количество строк текста, что смотреть и разбирать его не хочется (хотя, конкретно в этом примере не сложно разобраться): Но если добавить в него обнаружение наличия необходимой функциональности, то всё становится намного понятнее. Еще одним случаем необходимости такой диагностики является пример, когда на этапе компиляции требуется выбрать, какой метод у объекта должен быть вызван: В этих примерах можно было бы использовать концепты и рефлексию, но... В случае концептов их применение возможно только начиная с C++20 и их поддержка не везде реализована в полном объеме. А в случае с рефлексией маловероятно что её внедрение будет в C++23, если только в экспериментальных реализациях. А что делать, если такая функциональность нужна прямо здесь и сейчас? Для этого пока что подойдет эксплуатация механизма SFINAE, который уже традиционно используется в делах рефлексии, а не только по своему прямому назначению. С помощью механизма SFINAE в стандартной библиотеке в type_traits уже определено большое количество средств обнаружения различных особенностей для типов. Более широкий набор инструментов определен в Boost.TypeTraits, в том числе средства обнаружения операторов с помощью boost::has_<operator_name> и boost::is_detected. Наиболее часто рекомендуемым способом диагностировать наличие функциональности является применение конструкции вида: В этом случае компилятор выбирает подходящий метод __test, что позволяет косвенно диагностировать наличие желаемой функциональности. Но данный подход достаточно громоздкий и не наглядный в использовании. Замечательное предложение в стандарт N4502 (входит в состав Boost.TypeTraits), которое позволяет решить данную задачу весьма изящным способом. Не буду углубляться в техническую реализацию и принцип действия, они хорошо описаны в самом предложении N4502 и здесь. Углубимся в особенности его применения. Предложение содержит реализацию детектора для поддержки следующих желаемых компонентов из набора средств обнаружения:   is_detected В зависимости от возможности определения типа Op<Args...> является std::true_type или std::false_type. is_detected_exact В зависимости от результата проверки идентичности типа detected_t и типа Expected является std::true_type или std::false_type. is_detected_convertible В зависимости от возможности преобразования типа detected_t к типу To является std::true_type или std::false_type. detected_t В зависимости от возможности определения типа Op<Args...> является Op<Args...> или специальным типом nonesuch. detected_or_t В зависимости от возможности определения типа Op<Args...> является Op<Args...> или указанным типом Default. Рассмотрим подробнее, каким образом этим набором средств обнаружения можно пользоваться. Предположим, имеется следующие определения функций foo и требуется обнаружить их наличие с помощью представленных средств. Для этого необходимо определить вспомогательный тип: Данное определение означает, что FooFunction<Args...> является типом, возвращаемым функцией с параметрами foo(Args...) . Для эмуляции вызова функции foo с параметрами используется вспомогательный метод std::declval, с описанием которого можно ознакомится здесь. Используя это определение, можно обнаружить наличие функции foo следующим способом: А так как FooFunction<Args...> является типом, возвращаемым функцией с параметрами foo(Args...), то в этом случае можно проверить и возвращаемый тип на идентичность и на конвертируемость: Всё красиво и можно радоваться, но... Попробуем обнаружить функцию foo(double): И мы её обнаружили! Как же так? Всё дело в том, что при определении FooFunction<Args...> использовалась имитация вызова функции foo с подходящими параметрами, а не с идентичными. Это значит, что если существует возможность вызова функции foo с учетом правил преобразования типов, то функция будет обнаружена. Для строгого соответствия параметров при обнаружении функции следует использовать определение вида: Использование std::integral_constant позволяет гарантировать точное соответствие сигнатуры функции foo указанным типам для её параметров. Мы не указываем возвращаемый тип функции foo явно, поэтому задаем его с помощью типа detected_t<FooFunction, Args...>, который, как мы помним, и является типом, возвращаемым функцией с параметрами foo(Args...) или nonesuch. В этом случае обнаружение функции будет происходить строго в соответствии с сигнатурой: Есть несколько ограничений при обнаружении функций. Если определение функции foo будет произведено после определения типа FooFunction<Args...>, то она не будет обнаружена. Если до определения типа StrictFooFunction<Args...> не будет произведено ни одной декларации функции foo, то возникнет ошибка компиляции. Пусть имеется следующая декларация и требуется обнаружить наличие метода foo. Как и в случае для функции можно обнаружить метод с подходящими параметрами или с их строгим соответствием. При этом учитываются также квалификаторы доступа к функциям const и volatile. Для обнаружения метода с подходящими параметрами необходимо определить тип: Данное определение означает, что FooMember<Args...> является типом, возвращаемым функцией с параметрами T::foo(Args...) . Используя это определение, можно обнаружить наличие методаfoo следующим способом: Для обнаружения метода в строгом соответствии с сигнатурой есть нюансы. Если требуется обнаружить наличие статического метода в соответствии со строгой сигнатурой, то достаточно использовать определение типа, как для функции: В этом случае обнаружение будет выглядеть так: Если же необходимо обнаружить наличие не статического метода в строгом соответствии с сигнатурой, то требуется небольшая доработка в виде вспомогательного инструмента по определению типа метода члена по заданной сигнатуре. В этом случае определение вспомогательного типа для обнаружения не статического метода будет выглядеть так: Конструкция выглядит устрашающей). Но по сути своей эмулирует попытку вызова метода T::foo(Args ...) по её адресу с проверкой сигнатуры с помощью применения std::integral_constant. Обнаружение наличия метода с помощью StrictFooMember будет выглядеть уже привычным способом: Описанные ранее ограничения при обнаружении функций не распространяются на обнаружение членов класса. Единственным ограничением обнаружения членов класса является публичный доступ к ним. Проверка возвращаемого типа на идентичность и на конвертируемость для всех представленных определений может быть произведена с помощью функций is_detected_exact и is_detected_convertible. Набор инструментов обнаружения функциональности на этапе компиляции, представленные в предложении в стандарт N4502, позволяют достаточно гибко обнаруживать наличие методов, как подходящих, так и строго соответствующих заданной сигнатуре. Данный механизм может быть использован как альтернатива элементам концептов и рефлексии там, где последние еще не реализованы. Надеюсь, что кому-то помог разобраться в нюансах использования представленного инструмента обнаружения функциональности. Детальный пример использования подобного подхода представлен в моем проекте инструментов ScL (Detection). Также этот подход применяется при рефлексии операторов для класса-обертки, представленной в статье "Добавляем дополнительные особенности реализации на C++ с помощью «умных» оберток". Разработчик ПО