В этой статье хотелось бы кратко рассмотреть особенности применения механизма обнаружения наличия функциональности у используемых типов данных на этапе компиляции.
В большей степени такое обнаружение необходимо для возможности формирования вразумительных сообщений об ошибках в обобщенном программировании с помощью шаблонов, а также для унификации и оптимизации реализуемых алгоритмов.
Например, простейший код при компиляции выдаст такое количество строк текста, что смотреть и разбирать его не хочется (хотя, конкретно в этом примере не сложно разобраться):
class A {}; template <class T> void print ( const T & value ) { std::cout << "value: " << value << std::endl; } int main ( int, char ** ) { print( A() ); return 0; }
Но если добавить в него обнаружение наличия необходимой функциональности, то всё становится намного понятнее.
template <class T> std::enable_if_t< is_detected< LeftShiftOperator, std::ostream, T >(), void > print ( const T & value ) { std::cout << "value: " << value << std::endl; }
Еще одним случаем необходимости такой диагностики является пример, когда на этапе компиляции требуется выбрать, какой метод у объекта должен быть вызван:
template <class T> void clear ( T & container ) { if constexpr ( is_detected< ClearMember, T >() ) container.clear(); else if ( is_detected< CleanMember, T >() ) container.clean(); else if ( is_detected< RemoveAllMember, T >() ) container.removeAll(); }
В этих примерах можно было бы использовать концепты и рефлексию, но... В случае концептов их применение возможно только начиная с C++20 и их поддержка не везде реализована в полном объеме. А в случае с рефлексией маловероятно что её внедрение будет в C++23, если только в экспериментальных реализациях.
А что делать, если такая функциональность нужна прямо здесь и сейчас? Для этого пока что подойдет эксплуатация механизма SFINAE, который уже традиционно используется в делах рефлексии, а не только по своему прямому назначению. С помощью механизма SFINAE в стандартной библиотеке в type_traits уже определено большое количество средств обнаружения различных особенностей для типов. Более широкий набор инструментов определен в Boost.TypeTraits, в том числе средства обнаружения операторов с помощью boost::has_<operator_name> и boost::is_detected.
Наиболее часто рекомендуемым способом диагностировать наличие функциональности является применение конструкции вида:
void foo (); template <class T, class... Args> struct DoesFooFunctionExistsHelper { private: template <class _Test, class = decltype(foo(std::declval<Args>() ...) )> static constexpr ::std::true_type __test(int); template <class> static constexpr ::std::false_type __test(...); public: using type = decltype(__test<T>(std::declval<int>())); }; template < typename ... _Arguments > inline constexpr bool doesFooFunctionExists () { return DoesFooFunctionExistsHelper< void, _Arguments ... >::type::value; } static_assert( doesFooFunctionExists<>(), "The function foo() was declared but not detected!" );
В этом случае компилятор выбирает подходящий метод __test, что позволяет косвенно диагностировать наличие желаемой функциональности. Но данный подход достаточно громоздкий и не наглядный в использовании.
Инструменты обнаружения
Замечательное предложение в стандарт N4502 (входит в состав Boost.TypeTraits), которое позволяет решить данную задачу весьма изящным способом. Не буду углубляться в техническую реализацию и принцип действия, они хорошо описаны в самом предложении N4502 и здесь. Углубимся в особенности его применения.
Предложение содержит реализацию детектора для поддержки следующих желаемых компонентов из набора средств обнаружения:
template <template<class...> class Op, class... Args> using is_detected = ...; // std::true_type || std::false_type template< template<class...> class Op, class... Args > using detected_t = ...; // Op<Args...> || nonesuch template< class Default, template<class...> class Op, class... Args > using detected_or_t = ...; // Op<Args...> || Default template <class Expected, template<class...> class Op, class... Args> using is_detected_exact = std::is_same<detected_t<Op, Args...>, Expected>; template <class To, template<class...> class Op, class... Args> using is_detected_convertible = std::is_convertible<detected_t<Op, Args...>, To>;
| В зависимости от возможности определения типа |
| В зависимости от результата проверки идентичности типа |
| В зависимости от возможности преобразования типа |
| В зависимости от возможности определения типа |
| В зависимости от возможности определения типа |
Рассмотрим подробнее, каким образом этим набором средств обнаружения можно пользоваться.
Обнаружение функции
Предположим, имеется следующие определения функций foo
void foo(); int foo(int);
и требуется обнаружить их наличие с помощью представленных средств.
Для этого необходимо определить вспомогательный тип:
template <class... Args> using FooFunction = decltype( foo( std::declval<Args>() ... ) );
Данное определение означает, что FooFunction<Args...> является типом, возвращаемым функцией с параметрами foo(Args...) . Для эмуляции вызова функции foo с параметрами используется вспомогательный метод std::declval, с описанием которого можно ознакомится здесь.
Используя это определение, можно обнаружить наличие функции foo следующим способом:
static_assert( is_detected< FooFunction >(), "The foo() was defined but not detected!" ); static_assert( is_detected< FooFunction, int >(), "The foo(int) was defined but not detected!" ); static_assert( !is_detected< FooFunction, int, double >(), "The foo(int,double) was not defined but detected!" ); static_assert( !is_detected< FooFunction, std::string >(), "The foo(string) was not defined but detected!" );
А так как FooFunction<Args...> является типом, возвращаемым функцией с параметрами foo(Args...), то в этом случае можно проверить и возвращаемый тип на идентичность и на конвертируемость:
static_assert( is_detected_exact< int, FooFunction, int >(), "The int foo(int) was defined but not detected!" ); static_assert( !is_detected_exact< double, FooFunction, int >(), "The double foo(int) was not defined but detected!" ); static_assert( is_detected_convertible< double, FooFunction, int >(), "The convertible int foo(int) was defined but not detected!" );
Всё красиво и можно радоваться, но... Попробуем обнаружить функцию foo(double):
static_assert( is_detected< FooFunction, double >(), "The convenient foo(int) was defined but not detected!" ); // Oops!
И мы её обнаружили! Как же так?
Всё дело в том, что при определении FooFunction<Args...> использовалась имитация вызова функции foo с подходящими параметрами, а не с идентичными. Это значит, что если существует возможность вызова функции foo с учетом правил преобразования типов, то функция будет обнаружена.
Для строгого соответствия параметров при обнаружении функции следует использовать определение вида:
template <class... Args> using StrictFooFunction = decltype( std::integral_constant< detected_t<FooFunction, Args...>(*)(Args...), (&foo) >::value( std::declval<Args>() ... ) );
Использование std::integral_constant позволяет гарантировать точное соответствие сигнатуры функции foo указанным типам для её параметров. Мы не указываем возвращаемый тип функции foo явно, поэтому задаем его с помощью типа detected_t<FooFunction, Args...>, который, как мы помним, и является типом, возвращаемым функцией с параметрами foo(Args...) или nonesuch.
В этом случае обнаружение функции будет происходить строго в соответствии с сигнатурой:
static_assert( is_detected< StrictFooFunction, int >(), "The foo(int) was defined but not detected!" ); static_assert( is_detected< StrictFooFunction, double >(), "The foo(double) was not defined but detected!" );
Есть несколько ограничений при обнаружении функций.
Если определение функции foo будет произведено после определения типа FooFunction<Args...>, то она не будет обнаружена.
template <class... Args> using FooFunction = decltype( foo( std::declval<Args>() ... ) ); int foo(int); template <class... Args> using OtherFooFunction = decltype( foo( std::declval<Args>() ... ) ); static_assert( !is_detected< FooFunction, int >(), "The foo(int) was not defined before FooFunction but detected!" ); static_assert( is_detected< OtherFooFunction, int >(), "The foo(int) was defined before OtherFooFunction but not detected!" );
Если до определения типа StrictFooFunction<Args...> не будет произведено ни одной декларации функции foo, то возникнет ошибка компиляции.
Обнаружение метода структуры или класса
Пусть имеется следующая декларация
struct A { void foo(); int foo(int) const; static void foo(int, int); };
и требуется обнаружить наличие метода foo.
Как и в случае для функции можно обнаружить метод с подходящими параметрами или с их строгим соответствием. При этом учитываются также квалификаторы доступа к функциям const и volatile.
Для обнаружения метода с подходящими параметрами необходимо определить тип:
template <class T, class... Args> using FooMember = decltype(std::declval<T>().foo(std::declval<Args>() ...));
Данное определение означает, что FooMember<Args...> является типом, возвращаемым функцией с параметрами T::foo(Args...) . Используя это определение, можно обнаружить наличие методаfoo следующим способом:
static_assert( is_detected< FooMember, A >(), "The member A::foo() was declared but not detected!" ); static_assert( !is_detected< FooMember, A const >(), "The member A::foo() const was not declared but detected!" ); static_assert( is_detected< FooMember, A, int >(), "The member A::foo(int) const was declared but not detected!" ); static_assert( is_detected< FooMember, A const, double >(), "The convenient member A::foo(int) const was declared but not detected!" ); static_assert( is_detected< FooMember, A, int, int >(), "The static member A::foo(int,int) was declared but not detected!" );
Для обнаружения метода в строгом соответствии с сигнатурой есть нюансы.
Если требуется обнаружить наличие статического метода в соответствии со строгой сигнатурой, то достаточно использовать определение типа, как для функции:
template <class T, class... Args> using StrictFooStaticMember = decltype( std::integral_constant<detected_t<FooMember, T, Args ...>(*)(Args ...), &std::decay_t<T>::foo >::value(std::declval<Args>() ...) );
В этом случае обнаружение будет выглядеть так:
static_assert( is_detected< StrictFooStaticMember, A, int, int >(), "The static member void A::foo(int,int) was declared but not detected!" ); static_assert( !is_detected< StrictFooStaticMember, A, int&, int& >(), "The static member void A::foo(int&,int&) was not declared but detected!" );
Если же необходимо обнаружить наличие не статического метода в строгом соответствии с сигнатурой, то требуется небольшая доработка в виде вспомогательного инструмента по определению типа метода члена по заданной сигнатуре.
namespace detail { template <class T, class M> struct member_signature; template <class T, class R, class... Args> struct member_signature< T, R( Args ...) > { using type = R(std::decay_t<T>::*)(Args ...); }; template <class T, class R, class... Args> struct member_signature< T const, R(Args ...) > { using type = R(std::decay_t<T>::*)(Args ...) const; }; template <class T, class R, class... Args> struct member_signature< T const &, R(Args ...) > { using type = R(std::decay_t<T>::*)(Args ...) const &; }; template <class T, class R, class... Args> struct member_signature< T const &&, R(Args ...) > { using type = R(std::decay_t<T>::*)(Args ...) const &&; }; template <class T, class R, class... Args> struct member_signature< T volatile, R(Args ...) > { using type = R(std::decay_t<T>::*)(Args ...) volatile; }; template <class T, class R, class... Args> struct member_signature< T volatile &, R(Args ...) > { using type = R(std::decay_t<T>::*)(Args ...) volatile &; }; template <class T, class R, class... Args> struct member_signature< T volatile &&, R(Args ...) > { using type = R(std::decay_t<T>::*)(Args ...) volatile &&; }; template <class T, class R, class... Args> struct member_signature< T const volatile, R(Args ...) > { using type = R(std::decay_t<T>::*)(Args ...) const volatile; }; template <class T, class R, class... Args> struct member_signature< T const volatile &, R(Args ...) > { using type = R(std::decay_t<T>::*)(Args ...) const volatile &; }; template <class T, class R, class... Args> struct member_signature< T const volatile &&, R(Args ...) > { using type = R(std::decay_t<T>::*)(Args ...) const volatile &&; }; } template <class T, class Sign > using member_signature_t = typename detail::member_signature< T, Sign >::type;
В этом случае определение вспомогательного типа для обнаружения не статического метода будет выглядеть так:
template <class T, class... Args> using StrictFooMember = decltype( (std::declval<T>() .* std::integral_constant< member_signature_t<T, detected_t<FooMember, T, Args ...>(Args ...)>, &std::decay_t<T>::foo>::value)(std::declval<Args>() ...) );
Конструкция выглядит устрашающей). Но по сути своей эмулирует попытку вызова метода T::foo(Args ...) по её адресу с проверкой сигнатуры с помощью применения std::integral_constant.
Обнаружение наличия метода с помощью StrictFooMember будет выглядеть уже привычным способом:
static_assert( is_detected< StrictFooMember, A >(), "The member A::foo() was declared but not detected!" ); static_assert( !is_detected< StrictFooMember, A, int >(), "The member A::foo(int) was not declared but detected!" ); static_assert( is_detected< StrictFooMember, A const, int >(), "The member A::foo(int) const was declared but not detected!" ); static_assert( !is_detected< StrictFooMember, A const, double >(), "The member A::foo(double) const was not declared but detected!" );
Описанные ранее ограничения при обнаружении функций не распространяются на обнаружение членов класса. Единственным ограничением обнаружения членов класса является публичный доступ к ним.
Проверка возвращаемого типа на идентичность и на конвертируемость для всех представленных определений может быть произведена с помощью функций is_detected_exact и is_detected_convertible.
Выводы
Набор инструментов обнаружения функциональности на этапе компиляции, представленные в предложении в стандарт N4502, позволяют достаточно гибко обнаруживать наличие методов, как подходящих, так и строго соответствующих заданной сигнатуре.
Данный механизм может быть использован как альтернатива элементам концептов и рефлексии там, где последние еще не реализованы.
PS
Надеюсь, что кому-то помог разобраться в нюансах использования представленного инструмента обнаружения функциональности.
Детальный пример использования подобного подхода представлен в моем проекте инструментов ScL (Detection). Также этот подход применяется при рефлексии операторов для класса-обертки, представленной в статье "Добавляем дополнительные особенности реализации на C++ с помощью «умных» оберток".
