Tag Archives: enable_if

Conar 4: больше SFINAE

Начало

Предыдущая часть

Займёмся функциями, обеспечивающими чтение последовательностей. Первой такой функцией у меня будет не seq, а вариант value, который записывает каждое распознанное значение по итератору вывода.

template <class T, class OutIt> inline
auto value(OutIt dest,
  Possible_keys keys, Option_info info = {},
  std::enable_if_t<
    std::is_base_of<
      std::output_iterator_tag,
    typename std::iterator_traits<OutIt>::iterator_category>
      ::value, int> = 0)
{
  using std::move;
  return value_parser<T>(
    [dest](T val) mutable { *dest++ = std::move(val); },
    move(keys), move(info));
}

Мелкая неприятность: пользователь должен явно указывать тип считываемых значений T. В принципе, можно сделать два варианта: аналогичный приведённому выше и выводящий тип значения с помощью iterator_traits::value_type. К сожалению, стандартные итераторы вывода, реализующие вставку в контейнеры, определяют value_type как void, делая тип элементов контейнера невыводимым для функции value. Кроме того, заметим, что forward_iterator_tag наследует от input_iterator_tag, но не от output_iterator_tag.

Введём простенькие вспомогательные определения.

/// Check if iterator It is compatible with category Cat.
template <class It, class Cat>
using Has_iterator_category =
  is_base_of<Cat,
    typename iterator_traits<It>::iterator_category>;

/// Check if iterator It is an output iterator.
template <class It>
using Is_output_iterator =
  Has_iterator_category<It,
    output_iterator_tag>;

/// Check if iterator It is a forward iterator.
template <class It>
using Is_forward_iterator =
  Has_iterator_category<It,
    forward_iterator_tag>;

/// Check if iterator It declares void as its value type.
template <class It>
using Has_void_value =
  is_same<void,
    typename iterator_traits<It>::value_type>;

Теперь два варианта value, пишущих через итераторы можно определить следующим образом:

/// Construct a value option, which writes values through an iterator.
/// Explicitly typed variant when the type is not derivable from OutIt.
template <class T, class OutIt> inline
auto value(OutIt dest,
  Possible_keys keys, Option_info info = {},
  std::enable_if_t<
    impl::Is_output_iterator<OutIt>::value &&
    impl::Has_void_value<OutIt>::value,
    int> = 0)
{
  using std::move;
  return value_parser<T>(
    [dest](T val) mutable { *dest++ = std::move(val); },
    move(keys), move(info));
}

/// Construct a value option, which writes values through an iterator.
template <class OutIt> inline
auto value(OutIt dest,
  Possible_keys keys, Option_info info = {},
  std::enable_if_t<
    (impl::Is_output_iterator<OutIt>::value ||
     impl::Is_forward_iterator<OutIt>::value) &&
    !impl::Has_void_value<OutIt>::value,
    int> = 0)
{
  using std::move;
  using T = typename std::iterator_traits<OutIt>::value_type;
  return value_parser<T>(
    [dest](T val) mutable { *dest++ = std::move(val); },
    move(keys), move(info));
}

Тестирование этих определений вскрыло небольшое упущение: value_parser также должна создавать mutable-замыкание, если мы хотим упаковать в него своё mutable-замыкание, что требуется, в свою очередь, из-за необходимости передвигать итератор на каждом вызове и сохранять его изменённую версию между вызовами. Соответственно, я также добавил mutable в flag_parser и seq_parser.

Теперь можно перейти к seq.

Вариант seq, заполняющий условно бесконечную последовательность по итератору, написать не составит большого труда. Полная аналогия с вариантами value, указанными выше. Как и в случае с value, предполагается, что «места хватит» (интересно, сколько параметров командной строки можно скормить в распространённых ОС/интерпретаторах?).

Приведу одну из них. Вторая абсолютно аналогична.

template <class T, class OutIt> inline
auto seq(OutIt dest,
  Possible_keys keys, Option_info info = {},
  std::enable_if_t<
    impl::Is_output_iterator<OutIt>::value &&
    impl::Has_void_value<OutIt>::value,
    int> = 0)
{
  using std::move;
  return seq_parser<T>(
    [dest](T *from, T *to) mutable
    {
      while (from != to)
        *dest++ = std::move(*from++);
    },
    move(keys), move(info));
}

Для удобства пользователя можно добавить ещё три варианта seq, которые принимают непосредственно контейнеры, извлекают тип элементов через вложенное объявление value_type и выбирают способ вставки (push_back, insert или push) в зависимости от того, какой подходит. Для различения между push_back и insert (линейными и ассоциативными контейнерами) нам в качестве заготовки пригодится SFINAE-костыль Is_incrementable. Последний вариант seq подразумевает вставку в адаптер вроде std::stack с помощью вызова push.

/// Check if container C supports push_back(T&&).
template <class C, class T>
struct Supports_push_back
{
  template <class Y>
  static true_type check(decltype((void)(
    declval<Y>().push_back(declval<T>())
  ), 1));

  template <class Y>
  static false_type check(unsigned);

  using type = decltype(check<C&>(1));
  static constexpr bool value = type::value;
};

Два других детектора (Supports_insert и Supports_push) полностью аналогичны (да, макрос сюда так и просится). Вспомогательные определения для краткости записи сигнатур функций seq:

template <class C>
using Use_push_back =
  Supports_push_back<C, typename C::value_type>;

template <class C>
using Use_insert = bool_constant<
  Supports_insert<C, typename C::value_type>::value &&
  !Use_push_back<C>::value>;

template <class C>
using Use_push = bool_constant<
  Supports_push<C, typename C::value_type>::value &&
  !Use_push_back<C>::value &&
  !Use_insert<C>::value>;

Теперь вариант seq, который заполняет контейнер, предоставляющий функцию push_back, может быть записан следующим образом:

/// Construct a seq option,
/// which appends a "push_back" container.
template <class Container> inline
auto seq(Container &cont,
  Possible_keys keys, Option_info info = {},
  std::enable_if_t<
    impl::Use_push_back<Container>::value,
    int> = 0)
{
  using std::move;
  using T = typename Container::value_type;
  return seq_parser<T>(
    [&cont](T *from, T *to) mutable
    {
      while (from != to)
        cont.push_back(std::move(*from++));
    },
    move(keys), move(info));
}

Ещё два варианта seq полностью аналогичны.

Код данного варианта целиком.

Продолжение следует.

Conar 3: перегрузка функций и SFINAE

Часть 1

Часть 2

Написанный к данному моменту код всё ещё ничего не делает. Попробуем сделать функции-«упаковщики», создающие опции на основе заданных обработчиков и значений Possible_keys и Option_info. Роль DTO будет выполнять std::tuple<Обработчик, Possible_keys, Option_info>. Всего предполагается три семейства функций, отвечающих трём видам опций (ранее трём классам): flag, value и seq.

Предполагается, что данные функции будут перегружены. Для «выключения» ненужных шаблонов, вызывающих проблемы с неединственностью возможного варианта вызова перегруженной функции, C++11 и новее предлагает std::enable_if. Однако здесь нам неудобно ставить enable_if «впереди» (в возвращаемом типе), т.к. функции возвращают кортеж, включающий член анонимного типа. Впрочем, enable_if всегда можно поставить последним параметром.

Базовые функции имеют следующий вид:

/// Create a flag option with a custom handler.
template <class FlagHandler> inline
auto flag(FlagHandler handler,
  Possible_keys keys, Option_info info = {},
  std::enable_if_t<
    impl::Is_flag_handler<FlagHandler>::value, int> = 0)
{
  using std::move;
  return std::make_tuple(
    [f = move(handler)](auto from, auto to)
    {
      f();
      return from;
    },
    move(keys), move(info));
}

/// Create a value option with a custom handler.
template <class T, class ValueHandler> inline
auto value(ValueHandler handler,
  Possible_keys keys, Option_info info = {},
  std::enable_if_t<
impl::Is_value_handler_for<ValueHandler, T>::value, int> = 0)
{
  using std::move;
  return std::make_tuple(
    [f = move(handler)](auto from, auto to)
    {
      if (from == to)
        return from;

      std::istringstream reader(*from);
      T val {};
      if (reader >> val)
      {
        f(std::move(val));
        ++from;
      }

      return from;
    },
    move(keys), move(info));
}

/// Create a value sequence option with a custom handler.
template <class T, class SeqHandler> inline
auto seq(SeqHandler handler,
  Possible_keys keys, Option_info info = {},
  std::enable_if_t<
    impl::Is_seq_handler_for<SeqHandler, T>::value, int> = 0)
{
  using std::move;
  return std::make_tuple(
    [f = move(handler)](auto from, auto to)
    {
      std::vector<T> values;
      for (std::istringstream reader; from != to; ++from)
      {
        reader.str(*from);
        T val {};
        if (!(reader >> val))
          break;

        values.emplace_back(std::move(val));
        reader.clear();
      }

      f(values.data(), values.data() + values.size());
      return from;
    },
    move(keys), move(info));
}

Теперь определим их перегруженные варианты для конкретных простых случаев.

Простейший случай — установка булевской переменной в соответствии с присутствием заданного флага в опциях.

/// Construct a flag option, which sets a Boolean variable.
inline auto flag(bool &flag_var,
  Possible_keys keys, Option_info info = {})
{
  using std::move;
  return flag(
    [&flag_var]() { flag_var = true; },
    move(keys), move(info));
}

Более хитрый случай — подсчёт количества повторения заданного флага среди набора аргументов командной строки. Для этого будем передавать объект, для которого определён инкремент. Чтобы определить, доступна ли операция инкремента, воспользуемся стандартным костылём: SFINAE (принцип действия std::enable_if тоже основан на SFINAE). Вид этого костыля вызывает стойкое желание оформить соответствующий макрос…

template <class C>
struct Is_incrementable
{
  template <class Y>
  static true_type check(decltype(
    (void)(++declval<add_lvalue_reference_t<Y>>()), 1));

  template <class Y>
  static false_type check(unsigned);

  using type = decltype(check<C>(1));
  static constexpr bool value = type::value;
};

Собственно, код-«детектор» это ++declval<add_lvalue_reference_t<Y>>(), всё остальное — «boilerplate code». std::declval<Y> по умолчанию возвращает rvalue-ссылку, для которой ++ не определён, поэтому здесь следует предварительно сформировать «обычную» (lvalue) ссылку.

/// Construct a flag option, which increments a variable.
template <class C> inline
auto flag(C &counter,
  Possible_keys keys, Option_info info = {},
  std::enable_if_t<
    impl::Is_incrementable<C>::value &&
    !impl::Is_flag_handler<C>::value, int> = 0)
{
  return flag(
    [&counter]() { ++counter; },
    move(keys), move(info));
}

С Concepts этих корявеньких enable_if бы не было, да и прочие SFINAE-обороты можно было бы привести к менее костыльному виду. Но Concepts всё никак не принимают в стандарт…

Теперь займёмся value.
Запись прочитанного значения в заданную переменную могла бы иметь примерно такой вид:

/// Construct a value option, which sets a variable.
template <class T> inline
auto value(T &var,
  Possible_keys keys, Option_info info = {})
{
  return value<T>(/* что-то */);
}

Но данная сигнатура недостаточно хорошо «отличима» от сигнатуры базовой версии функции value… Я вижу три варианта решения проблемы: а) заставить пользователя указывать тип считываемого значения явно и проверять приводимость к T; б) ввести специальную функцию var, порождающую объект-обёртку переменной; в) проверять «читабельность» значений T из потока ввода (написать SFINAE-детектор аналогичный Is_incrementable и воспользоваться enable_if).

Вариант (б) менее интуитивен и вообще не очень мне нравится. Вариант (в) технически менее изящен, но более удобен для пользователя — не надо указывать тип явно.

Наконец, можно просто дать особые имена базовым функциям flag, value и seq, удалив их из набора перегруженных «удобных» функций прямого использования. Именно так я и поступлю. Теперь базовые версии flag, value и seq носят имена flag_parser, value_parser и seq_parser.

/// Construct a value option, which sets a variable.
template <class T> inline
auto value(T &var,
  Possible_keys keys, Option_info info = {})
{
  using std::move;
  return value_parser<T>(
    [&var](T val) { var = std::move(val); },
    move(keys), move(info));
}

В принципе, сейчас уже можно написать простенькие тесты на введённые выше «удобные» функции. Тесты добавлены в namespace test заголовочного файла conar.hpp. Тесты возвращают 0 в случае успеха. Объём кода тихонечко перевалил за 300 строчек. Интересно, в 1000 строк полная версия влезет?

Код данного варианта целиком.

Продолжение следует.