std::is_same для оператора цикла

#c #templates #constexpr

Вопрос:

У меня есть две структуры с методами, возвращающими итераторы в начале и в конце коллекции объектов, которыми они владеют. Методы имеют разные имена (это может показаться плохой архитектурой приложения, но это всего лишь упрощенная модель).:

 struct A {
  std::vector<int>::iterator a_begin() { return v.begin(); }
  std::vector<int>::iterator a_end() { return v.end(); }

  std::vector<int> v = { 1, 2 };
};

struct B {
  std::vector<float>::iterator b_begin() { return v.begin(); }
  std::vector<float>::iterator b_end() { return v.end(); }

  std::vector<float> v = { 1.0f, 2.0f };
};
 

Я хочу написать шаблонную функцию, которая будет повторять данный объект (типа A или типа B) и выполнять некоторую работу с его элементами. Мой подход таков:

 template<class T>
void foo(T t) {
  if constexpr (std::is_same_v<T, A>) {
    for (auto it = t.a_begin(); it != t.a_end(); it  ) {
      // a lot of stuff
    }
  } else if constexpr (std::is_same_v<T, B>) {
    for (auto it = t.b_begin(); it != t.b_end(); it  ) {
      // the same stuff
    }
  }
}
 

Это выглядит немного некрасиво для меня, потому for что тела петель одинаковы. Есть ли какой-нибудь способ улучшить это?

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

1. Почему у этих классов есть такие функции, как a_begin() и нет begin() ? Если бы они следовали стандартным соглашениям, вы могли бы использовать их без каких if constexpr -либо ограничений , потенциально даже с циклом на основе диапазона.

2. Почему бы не поместить этот повторяющийся код в функцию? Это обычное решение для повторяющейся логики, и оно будет работать в вашем случае.

Ответ №1:

Я принимаю ваше утверждение об именовании и сложности за чистую монету, такое абстрактное и связное.

 namespace detail {
    inline auto foo_begin(Aamp; a) { return a.a_begin(); }
    inline auto foo_end  (Aamp; a) { return a.a_end();   }
    inline auto foo_begin(Bamp; b) { return b.b_begin(); }
    inline auto foo_end  (Bamp; b) { return b.b_end();   }
}

template<class T>
void foo(T t) {
    for (auto it = detail::foo_begin(t); it != detail::foo_end(t);   it) {
      // the same stuff
    }
}
 

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

Если вы делаете это часто, возможно, стоит рассмотреть адаптер диапазона. Вы можете написать его вручную, или с помощью C 20 std::ranges::subrange вы даже можете использовать этот набор перегрузки сам по себе.

 template<class T>
void foo(T t) {
    for (auto amp;item : std::ranges::subrange(detail::foo_begin(t), detail::foo_end(t))) {
      // the same stuff
    }
}
 

Ответ №2:

Ключевая концепция итераторов заключается в том, что два итератора определяют последовательность. Вот и все, что нужно сделать: просто используйте пару итераторов, а не контейнер:

 template <class It>
void foo(It begin, It end) {
    while (begin != end) {
        // a lot of stuff
          begin;
    }
}
 

Теперь вы можете вызвать его с диапазоном, определяемым любым типом контейнера, который вам нравится:

 A a;
foo(a.a_begin(), a.a_end());

B b;
foo(b.b_begin(), b.b_end());
 

Ответ №3:

Возможно, это поможет переосмыслить вашу проблему.

 #include <vector>
#include <algorithm>

template<typename T>
void DoStuff(const Tamp; value)
{
};

template<typename T>
void DoAllStuffFor(const std::vector<T>amp; v)
{
    std::for_each(v.begin(), v.end(), DoStuff<T>);
}

int main()
{
    std::vector<int> v1 = { 1, 2 };
    std::vector<double> v2 = { 1, 2 };

    DoAllStuffFor(v1);
    DoAllStuffFor(v2);
}
 

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

1. В OP нет векторов, а только итераторы, поэтому я боюсь, что это не поможет.