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.
Как я понял, она реализует идеому обнаружения, которая, в свою очередь, реализует возможность обнаружить какую-то функцию в классе. Но что делать, если в классе две перегруженные функции, а нужно проверить наличие каждой?
И вот вопросы:
- Правильно ли я вообще понимаю эту идиому?
- Как проверить наличие всех перегруженных функций, через эту идиому?
- Или, если я всё-таки неправильно понял, что делает код обнаружения, подскажите, каким способом можно решить поставленную задачу.
- И главное, какая должна быть реализация у has_insert_t, если проверяемая функция имеет аргументы или несколько аргументов.
P.s. За клавиатурой стажёр, так что следующий пример может вызвать у вас приступ паники (или 1001 ошибку компиляции), будьте осторожны.
Я так понимаю, что-то такое?
template <class Ty, class... Args>
using has_insert_t = decltype(::std::declval<Ty::insert>(Args...))
Ответы (2 шт):
Итак, вот моё решение
Шаг 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();
}
Задача решена!
Отвечаю на ваши вопросы.
Вопрос 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. Но я обязательно им стану! :)