C++ Про идиому обнаружения, или изобретаем велосипед (concepts)

Общая постановка задачи:

Имеется 100500+ классов, которые имеют примерно схожую реализацию (30% контейнеров, 30% итераторов, и остальные специфичные).

Нужно:

Сделать общую реализацию для каждого типа классов.

Пример:

Возьмем в качестве типа классов класс-контейнер.

template <class Ty>
class container{
  ...
}

Любой контейнер должен содержать:

using-и:

using value_type = Ty

функции:

begin()
end()
insert(const value_type&)
insert(value_type&&)

Решение:

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

Его функция очень простая. Вызвать static_assert для каждой функции из перечня. Проделывать он это будет с каждым классом-контейнером.

В итоге мы будем получать ошибку на стадии компиляции о том, что класс не соответствует перечню неких требований. Например, вместо функции insert, реализована функция Insert (заглавная) или не реализована совсем.

Шаг 1. Реализуем прототип готового класса-контейнера:

 template <class Ty>
    class container{
      using value_type = Ty;

      begin() {}
      end() {}
      insert(const value_type&) {}
      insert(value_type&&) {}
    } ;

Шаг 2. Возьмем код с сайта cppreference.com и добавим наш класс-контейнер.

namespace detail {
  template <class Default, class AlwaysVoid,
    template<class...> class Op, class... Args>
  struct detector {
    using value_t = std::false_type;
    using type = Default;
  };

  template <class Default, template<class...> class Op, class... Args>
  struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> {
    using value_t = std::true_type;
    using type = Op<Args...>;
  };

} // namespace detail

struct nonesuch {
  ~nonesuch()                     = delete;
  nonesuch(nonesuch const&)       = delete;
  void operator=(nonesuch const&) = delete;
};

template <template<class...> class Op, class... Args>
using is_detected = typename detail::detector<nonesuch, void, Op, Args...>::value_t;

// Наш КЛАСС_КОНТЕЙНЕР
template <class Ty>
class container{
  using value_type = Ty;

  begin() {}
  end() {}
  insert(const value_type&) {}
  insert(value_type&&) {}
}

// Проверка на существование функции insert
template <class T>
using has_insert_t = decltype(.....); // То место, которое я не понимаю, как реализовать.

class container_check{
  static_assert(is_detected<has_insert_t, container>::value, "!");  // нужно проверить insert(const value_type&)
  static_assert(is_detected<has_insert_t, container>::value, "!");  // нужно проверить insert(value_type&&)
};

Это пример реализации экспериментальной(?) функции is_detect.

Как я понял, она реализует идеому обнаружения, которая, в свою очередь, реализует возможность обнаружить какую-то функцию в классе. Но что делать, если в классе две перегруженные функции, а нужно проверить наличие каждой?

И вот вопросы:

  1. Правильно ли я вообще понимаю эту идиому?
  2. Как проверить наличие всех перегруженных функций, через эту идиому?
  3. Или, если я всё-таки неправильно понял, что делает код обнаружения, подскажите, каким способом можно решить поставленную задачу.
  4. И главное, какая должна быть реализация у has_insert_t, если проверяемая функция имеет аргументы или несколько аргументов.

P.s. За клавиатурой стажёр, так что следующий пример может вызвать у вас приступ паники (или 1001 ошибку компиляции), будьте осторожны.

Я так понимаю, что-то такое?

template <class Ty, class... Args>
using has_insert_t = decltype(::std::declval<Ty::insert>(Args...))

Ответы (2 шт):

Автор решения: Anton Mitrochin

Итак, вот моё решение

Шаг 1. Сначала проверим, что в классе-контейнере есть using value_type = ...

template<class Ty, class Using = void>
class has_value_type final : public std::false_type {};

template<class Ty>
class has_value_type<Ty, STD void_t<typename Ty::value_type>> final : public std::true_type {};

Эта конструкция позволяет узнать, есть ли объявление value_type в каком-то классе. Используем это позже.

Шаг 2. Теперь нужен код, который будет проверять функцию с конкретными параметрами.

template <class T, class... Ts>
using has_insert = decltype(std::declval<T>().insert(std::declval<Ts>()...));

Шаг 3. Пишем класс-проверщк.

template <class Ty>
class CONTAINER_CHECK {
public:
  CONTAINER_CHECK() = delete; // Экземпляры класса не нужно создавать, запретим конструктор.

  static check() {
      // Проверим, есть ли в классе value_type и iterator
    static_assert(has_value_type<Ty>::value, "!");
    static_assert(has_iterator<Ty>::value, "!");

    using value_type = typename Ty::value_type;
    using iterator = typename Ty::iterator;

    // Проверяем, есть ли обе перегруженные функции.
    static_assert(is_detected<has_insert, Ty, iterator, value_type&&>::value, "!");
    static_assert(is_detected<has_insert, Ty, iterator, const value_type&>::value, "!");
  }
};

Для ленивых полное решение ;)

namespace detail {
  template <class Default, class AlwaysVoid,
    template<class...> class Op, class... Args>
  struct detector {
    using value_t = std::false_type;
    using type = Default;
  };

  template <class Default, template<class...> class Op, class... Args>
  struct detector<Default, std::void_t<Op<Args...>>, Op, Args...> {
    using value_t = std::true_type;
    using type = Op<Args...>;
  };

} // namespace detail

struct nonesuch {
  ~nonesuch() = delete;
  nonesuch(nonesuch const&) = delete;
  void operator=(nonesuch const&) = delete;
};

template <template<class...> class Op, class... Args>
using is_detected = typename detail::detector<nonesuch, void, Op, Args...>::value_t;

// Наш КЛАСС_КОНТЕЙНЕР
template <class Ty>
class container {
  using value_type = Ty;

  begin() {}
  end() {}
  insert(const value_type&) {}
  insert(value_type&&) {}
}

// Проверка на существование функции insert
template <class T, class... Ts>
using has_insert = decltype(std::declval<T>().insert(std::declval<Ts>()...));

template <class Ty>
class CONTAINER_CHECK {
public:
  CONTAINER_CHECK() = delete; // Экземпляры класса не нужно создавать, запретим конструктор.

  static check() {
      // Проверим, есть ли в классе value_type и iterator
    static_assert(has_value_type<Ty>::value, "!");
    static_assert(has_iterator<Ty>::value, "!");

    using value_type = typename Ty::value_type;
    using iterator = typename Ty::iterator;

    // Проверяем, есть ли обе перегруженные функции.
    static_assert(is_detected<has_insert, Ty, iterator, value_type&&>::value, "!");
    static_assert(is_detected<has_insert, Ty, iterator, const value_type&>::value, "!");
  }
};

int main(){
  CONTAINER_CHECK<container>::check();
}

Задача решена!

→ Ссылка
Автор решения: Anton Mitrochin

Отвечаю на ваши вопросы.

Вопрос 1.

Ну, допустим написали , ну вот зачем он нужен? Как он может работать с объектами конкретного класса? Только чтобы проверить наличие метода? Если только в учебных целях...

В написании проекта (точнее подпроекта, где я работаю) участвуют +- 10 программистов. Каждый из которых написал и напишет еще по десятку контейнеров (к примеру). Каждый из них называет переменную типа хранимых данных по-своему. Кто-то, кто хочет соответствовать стандарту, назовет её value_type, тот, кто любит сокращать - назовёт value_t, а кто совсем уж любит покороче - vt. В этот список также можно добавить type_of_value, _valueType, ValueType и еще много-много вариантов.

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

Мы могли бы написать код:

using type = container::value_type;

Но вместо value_type в проекте каша из разных названий этой переменной во всех классах.

Мое решение позволяет вызвать статический метод, который проверит соответствие всех переменных и функций. Так как он работает в runtime и только один раз, на производительность это никаким образом не влияет.

Плюс к этому многие забывают объявлять нужные методы. Например реализовывают begin() и end(), а вот про cbegin, rbegin, crbegin, cend, rend, crend забывают. Это, опять же, усложняет задачу другим программистам, которые используют совмсетный код.

p.s.Мы не будем говорить о том, что тип можно узнать, получив данные из контейнера и узнав их тип. Мы сейчас абстрагировались и решаем общую проблему, ведь случай может быть намного сложнее.

Вопрос 2.

Мне кажется, или вы изобрели вело... Ну, в смысле, концепты?

Так и получается. Я пока ещё не ведущий разработчик компании и не могу заставить его перевести проект на c++20. Но я обязательно им стану! :)

→ Ссылка