вторник, 9 июля 2019 г.

[prog.c++] Что-то затупил с выписыванием условия для SFINAE

Стоило отвлечься на пару недель на написание документации и статей, как остаточные знания C++ улетучились из головы и случились жуткие тормоза при попытке написать условие для SFINAE :(

Нужно было вот что: в шаблонный фабричный метод передается ссылка на контейнер (или что-то, что притворяется контейнером, какой-нибудь range, к примеру). Нужно, чтобы этот шаблонный фабричный метод попадал в рассмотрение компилятора только если передали ссылку на контейнер/range. Для этого я решил написать проверку того, что у контейнера есть методы begin/end, и что при разыменовании итератора, возвращенного begin-ом, получается нужный мне тип.

Затык случился при попытке написать проверку на наличие begin/end. Вроде как в C++17 нет штатных средств проверить, что decltype(std::declval<const Another_Container &>().begin()) является валидным типом. Т.е. не нашел в type_traits чего-то вроде is_valid_v<constexpr-expr>. Пришлось извращаться вот таким образом:

templatetypename Another_Container >
static std::enable_if_t<
      !std::is_same_v< Container, Another_Container >
      && !std::is_same_v<
            decltype(std::declval<const Another_Container &>().begin()),
            void >
      && !std::is_same_v<
            decltype(std::declval<const Another_Container &>().end()),
            void >
      && std::is_same_v<
            std::decay_t<
                  decltype(*(std::declval<const Another_Container &>().begin())) >,
            mbox_t >,
      mbox_t >
make(
   environment_t & env,
   const Another_Container & destinations )
   {
      return make( env, destinations.begin(), destinations.end() );
   }

Т.е. сравнивать тип итератора, возвращаемого begin/end, с void. Ведь итератор, в принципе, не может иметь тип void. Поэтому если begin/end есть, то возвращать они должны не void.

Написать-то написал. И вроде как оно работает. Но не оставляет ощущение, что сильно туплю. И что можно как-то проще. Но вот как непонятно (особенно без введения собственных вспомогательных типов, аналогичных гипотетическому is_valid_v).

Upd. Обновленный текущий вариант, полученный в результате обсуждения в FB, пока выглядит так:

templatetypename Another_Container >
static std::enable_if_t<
      !std::is_same_v< Container, Another_Container >
      && std::is_convertible_v<
            decltype(
               ++std::declval<
                  std::add_lvalue_reference_t<
                     decltype(std::begin(std::declval<const Another_Container &>()))>
                  >()
               == std::end(std::declval<const Another_Container &>()) ),
            bool>
      && std::is_same_v<
            std::decay_t<
                  decltype(*std::begin(std::declval<const Another_Container &>())) >,
            mbox_t >,
      mbox_t >
make(

Здесь сделано упрощение + поддержка C-шных массивов + несколько дополнительных проверок, которых не было ранее, а должны были бы быть. И, что самое интересное, это даже VC++ компилируется (как VS2019, так и VS2017).

Upd.2 У варианта с SFINAE обнаружилось два серьезных недостатка. Первый, который был виден изначально, это отсутствие поддержки ADL. Т.е. если Another_Container -- это какой-то пользовательский тип, для которого пользователем определены собственные begin/end функции, то SFINAE бы его забраковал. Поскольку внутри SFINAE используются std::begin/std::end. И как внутри условия SFINAE разбираться с ADL без создания каких-то дополнительных вспомогательных сущностей -- это отдельный квест.

Второй недостаток -- это неспособность Doxygen-а сгенерировать документацию для метода с навороченными условиями SFINAE. Показанный в первом апдейте вариант кода Doxygen не может переворить и просто не включает такой make в итоговую документацию :(

Посему в итоге отказался от SFINAE, а все проверки перенес внутрь метода в static_assert-ы. Получилось что-то вроде:

templatetypename Another_Container >
static mbox_t
make(
   environment_t & env,
   const Another_Container & destinations )
   {
      using std::begin;
      using std::end;

      // Ensure that destinations if a container or range-like object
      // with mbox_t inside.
      static_assert(
         std::is_convertible_v<
            decltype(
               ++std::declval<
                  std::add_lvalue_reference_t<
                     decltype(begin(std::declval<const Another_Container &>()))>
                  >()
               == end(std::declval<const Another_Container &>()) ),
            bool>,
         "destinations should be a container or range-like object" );

      static_assert(
         std::is_same_v<
            std::decay_t<
                  decltype(*begin(std::declval<const Another_Container &>())) >,
            mbox_t >,
         "mbox_t should be accessible via iterator for destinations container (or "
         "range-like object" );

      return make( env, begin(destinations), end(destinations) );
   }

PS. Ну и современный C++ в очередной раз оставил впечатление вроде "ну круто, чё, а нельзя ли все тоже самое, но раза в полтора-два попроще?". Так что ждем C++20 с концептами.

3 комментария:

Pavel комментирует...

А почему detection idiom и `std::void_t` из C++17 не подходит? На cppreference пример как раз для begin/end : void_t

eao197 комментирует...

@Pavel
Если бы нужно было проверить только наличие begin/end, то void_t подошел бы. Но мне нужны и другие проверки, которые выдают bool.

Сергей Скороходов комментирует...

Это все потому что west const, был бы east const - все намного легче бы прлучилось...