Обнаружение наличия функциональности в C++ на этапе компиляции

от автора

В этой статье хотелось бы кратко рассмотреть особенности применения механизма обнаружения наличия функциональности у используемых типов данных на этапе компиляции.

В большей степени такое обнаружение необходимо для возможности формирования вразумительных сообщений об ошибках в обобщенном программировании с помощью шаблонов, а также для унификации и оптимизации реализуемых алгоритмов.

Например, простейший код при компиляции выдаст такое количество строк текста, что смотреть и разбирать его не хочется (хотя, конкретно в этом примере не сложно разобраться):

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>;

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

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++ с помощью «умных» оберток».


ссылка на оригинал статьи https://habr.com/ru/post/657261/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *