В этой статье хотелось бы кратко рассмотреть особенности применения механизма обнаружения наличия функциональности у используемых типов данных на этапе компиляции.
В большей степени такое обнаружение необходимо для возможности формирования вразумительных сообщений об ошибках в обобщенном программировании с помощью шаблонов, а также для унификации и оптимизации реализуемых алгоритмов.
Например, простейший код при компиляции выдаст такое количество строк текста, что смотреть и разбирать его не хочется (хотя, конкретно в этом примере не сложно разобраться):
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++ с помощью «умных» оберток".