Шаблонная магия, метафункция IsValidExpression

    Доброго времени суток, уважаемое Хабрасообщество.

    Сегодня я хочу поделиться одним интересным приемом, который позволяет определять компилируемость любого конкретного выражения.

    Пример:
    /* Определяем метафункцию HasF, которая позволяет определить наличие функции f() у любого класса. */
    DECLARE_IS_VALID_EXPRESSION(
        HasF,
        ( ( U * ) NULL )->f() /* Это выражение компилируемо только если присутствует U::f() */ );
     
    struct Foo{ void f(); };    
    struct Bar{};
     
    BOOST_STATIC_ASSERT( HasF< A >::value );  /* Тут константа HasF< A >::value будет true */
    BOOST_STATIC_ASSERT( !HasF< B >::value ); /* Тут константа HasF< A >::value будет false */

    Как Вы уже, наверное, догадались мы будем думать как написать макрос DECLARE_IS_VALID_EXPRESSION.

    Итак, наша цель — научиться определять, скомпилируется ли какое-либо выражение или нет. При этом компилятор, естественно, не должен выдавать никаких ошибок: должна просто генерироваться константа, со значением 0, если выражение некомпилируемо, и значением 1 в противном случае.

    Релизация


    Для этого мы будем использовать принцип SFINAE (substitution failure is not an error). На человеческом языке это означает, что если компилятор встречает «ошибку» внутри определения шаблона (не в теле шаблона, а в определении, т.е. в тех местах, где компилятор старается «подобрать» адекватные коду шаблонные параметры), то эта «ошибка» приводит не к ошибке компиляции, а к прекращению попытки инстанцировать шаблонную функцию (или класс) с теми параметрами, которые вызывают «ошибку».

    Именно так работает следующий код:
    $define DECLARE_IS_VALID_EXPRESSION( NAME, U_BASED_RUNTIME_EXPRESSION ) \
        template< class T > \
        struct NAME \
        { \
             /* Нам потребуется какой-нибудь тип, который точно не T для сравнения */ \
             struct CDummy{}; \
                              \
             /* Эта перегрузка будет работать только когда U_BASED_RUNTIME_EXPRESSION не содержит "ошибок" \
             ** В противном случае эта перегрузка будет проигнорирована согласно SFINAE. */
     \
             template< typename U > \
             static decltype( U_BASED_RUNTIME_EXPRESSION ) F( void * ); \
                                                                               \
             /* А вот эта перегрузка присутствует всегда, но приоритет ее ниже, потому как троеточие */ \
             template< typename U > \
             static CDummy F( ... ); \
                                            \
             /* Этого typedef могло бы и не быть, но без него этот класс работает неправильно :( \
             ** (пользуясь случаем передаю привет тестерам комманды Visual Studio) */
     \
             typedef decltype( F< T >( nullptr ) ) \
                 TDummy; \         
                         \
             enum \
             { \
                 /* value будет 1, если U_BASED_RUNTIME_EXPRESSION не содержит "ошибок" и 0 в противном случае \
                 ** Почему? \
                 ** Если "ошибок" нету, то присутвуют обе версии F, и F< T >( nullptr ) выбирает ту, \
                 ** в которой нету троеточия, т.е. с нашим тестируемым выражением, а ее возвращаемый тип никак
                 ** не CDummy, т.к. CDummy объявлен локально. \
                 ** Если же "ошибки" есть, то вариант F с тестируемым выражением будет выкинут, и, \
                 ** соответственно, F< T >( nullptr ) выберет вторую перегрузку (которая возвращает CDummy) */
     \
                 value = !boost::is_same< CDummy, TDummy >::value \
             }; \
        };

    Данная реализация, к сожалению, требует наличия C++0x (мой компилятор — VC10). Теоретически возможно обойтись и без нового стандарта (идея та же, но вместо decltype используется sizeof). Но! Здесь я снова хочу передать привет тестерам из Майкрософт, т.к. sizeof работает неправильно в области определения шаблона — он там «не ожидается» (если я правильно помню). В gcc решение на sizeof работает нормально.

    Применение


    Примером применения может служить, например, следующий код:
    /* Определяет метафункцию IsStreamSerializationSupported, которая возвращает
    ** true, если аргумент поддерживат ввод/вывод через потоки */

    DECLARE_IS_VALID_EXPRESSION(
        IsStreamSerializationSupported,
        ( (std::cout << *(*)NULL)(std::cin >> *(*)NULL) ) );
     
    /* double поддерживает ввод/вывод через потоки "из коробки" */
    BOOST_STATIC_ASSERT( IsStreamSerializationSupported< double >::value );
     
    struct Foo{};
     
    /* А вот Foo ввод/вывод через потоки не поддерживает :( */
    BOOST_STATIC_ASSERT( !IsStreamSerializationSupported< Foo >::value );
     
    struct Bar{};
     
    template< class TChar, class Traits >
    std::basic_ostream< TChar, Traits > &operator<<(
        std::basic_ostream< TChar, Traits > &const Bar & );
     
    template< class TChar, class Traits >
    std::basic_istream< TChar, Traits > &operator>>(
        std::basic_istream< TChar, Traits > &, Bar & );
     
    /* Bar поддерживает ввыод/вывод через потоки, т.к. определены соответствующие операторы. */
    BOOST_STATIC_ASSERT( IsStreamSerializationSupported< Bar >::value );

    Такие штуки помогают при проверки соответствия переданного в шаблон типа различным концептам (в данном случае для соответствия концепту тип должен поддерживать ввод/вывод через потоки).

    За сим я прощаюсь, всем спасибо за внимание! :)

    Комментарии 14

    • НЛО прилетело и опубликовало эту надпись здесь
        0
        Не за что :)

        Вообще, идея использовать SFINAE для таких вещей из boost::mpl (там есть подобный макрос, который позволяет определить наличие типа с именем X внутри класса). Это некое обобщение, которое позволяет проверять не только типа, а практически все, что пожелает душа :)
        0
        Вот в такие моменты особенно D :)
        Но красиво, да.
          +2
          Слово пропустили, но таки да. В такие моменты особенно D
            +1
            И вовсе ничего я не пропускал.
            D здесь — наречие :)
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Как я уже писал выше, has_xxx не позволяет проверять что угодно, конкретно boost::mpl умеет:
            1. Проверять наличие типа с именем XXX внутри класса
            2. Проверять наличие шаблонного типа template<… > class XXX внутри класса.

            Оба примера, которые есть в посте (проверка наличия функции f()) и проверка, на возможность ввода/вывода через потоки не реализуемы на бусте.
          • НЛО прилетело и опубликовало эту надпись здесь
              +3
              прошу прощения, не writeonly ли..? :)
                0
                +1, сколько сам писал, напишешь что-нить сложное, работает, надо поправить, проще с нуля написать, чем разобраться, что там было написано.
                • НЛО прилетело и опубликовало эту надпись здесь
                +2
                попробовал придумать решение не требующее C++0x, получилось что-то типа этого:
                только под msvc-8.0 и msvc-9.0 выдается результат 1010 (правильный), а под gcc 4.4.0 — 1000
                #include <iostream>
                
                typedef char Small;
                class Big {char dummy[2]; };
                
                template <size_t> struct ClassSelector {
                    typedef Big type_;
                };
                
                template <> struct ClassSelector< sizeof(1)/sizeof(1) > {
                    typedef Small type_;
                };
                
                #define DECLARE_IS_VALID_EXPRESSION( NAME, U_BASED_RUNTIME_EXPRESSION )\
                template< class T > \
                struct NAME\
                {\
                    template< typename U > static typename ClassSelector< sizeof( U_BASED_RUNTIME_EXPRESSION, 1 ) / sizeof( U_BASED_RUNTIME_EXPRESSION, 1 )>::type_ F( void * );\
                    template< typename U > static typename ClassSelector<0>::type_ F( ... );\
                    enum {  is_valid =  ( sizeof( F<T>( NULL ) ) != sizeof( ClassSelector<0>::type_ ) ) };\
                };
                
                struct Foo{ void f(); };
                struct Bar{};
                
                DECLARE_IS_VALID_EXPRESSION(HasF,  ( ( U * ) NULL )->f() );
                
                DECLARE_IS_VALID_EXPRESSION(
                    IsStreamSerializationSupported,
                    ( (std::cout << *(U *)NULL), (std::cin >> *(U *)NULL) ) );
                
                int main(int argc, char *argv[])
                {
                    std::cout  << HasF<Foo>::is_valid <<
                            HasF<Bar>::is_valid <<
                            IsStreamSerializationSupported<double>::is_valid <<
                            IsStreamSerializationSupported<Bar>::is_valid ;
                    return 0;
                }
                
                  0
                  Не совсем понял как можно использовать этот шаблон для определения языковых конструкции, которые не должны компилироваться. Возможно ли это? В тестах такое бывает необходимо.
                    0
                    Дело в том, что в C++ есть 2 уровня проверки:

                    1. Минимальная проверка синтаксиса шаблона. Если шаблон ее проходит, значит есть ненулевая вероятность, что после инстанцирования его какими-либо типами/константами результат пройдет проверку №2.
                    2. Полная проверка выражений во время инстанцирования шаблона.

                    DECLARE_IS_VALID_EXPRESSION(...)
                    определяет шаблон, и к нему применяется только проверка №1.

                    BOOST_STATIC_ASSERT( HasF< A >::value )
                    инстанцирует шаблон HasF параметром A, применяется проверка №2.

                    Из этого следует, что если вы вставите на проверку код, где забыли закрыть скобку, использовали ключевое слово не так, как полагается и пр. шалости, код будет ill-formed. А код ( ( U * ) NULL )->f() вполне проходит проверку №1, потому что теоретически может существовать тип с методом f().
                    BOOST_STATIC_ASSERT( HasF< std::string >::value ) не проходит проверку №2, но по правилу SFINAE код компилируется.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое