Применение std::function к шаблону дизайна посетителя
Я немного новичок в ood. Читая шаблоны дизайна GoF, я нашел посетителя.
Моя версия шаблона посетителя является более конкретной, чем упомянуто в " Универсальном посетителе, использующем шаблоны с переменным числом мест". Итак, моя идея состоит в том, чтобы создать конкретного посетителя, имея std::function
s, которые будут поставляться во время строительства. Затем каждая функция посещения будет вызывать соответствующий частный std::function
,
Мой вопрос: это хорошая практика для реализации посетителя, как указано выше, или если нет, то почему?
Единственные минусы, которые приходят на ум, - это двусмысленность, то есть будет трудно понять, что конкретный экземпляр посетителя будет делать с композитом.
2 ответа
Как вы реализуете посетителя с std::function
Посетители должны изменить принимаемую часть элемента. Вы теряете двойную диспетчеризацию как стоимость, но вы немного абстрагируете шаблон итерации.
Вместо одного accept
метод на элементе, есть один accept
за вид посещения.
Если вы хотите посещать вещи несколькими способами у стандартного посетителя, вы пишете больше типов посетителей и добавляете новые accept
перегрузки, чтобы принять их.
в std::function
основанный один, вы просто пишете новый accept
функция type с другим именем; имя указывается в имени метода, а не в имени типа посетителя (поскольку тип посетителя является анонимным).
В C++14 со SFINAE std::function
Смартс, вы можете пойти с одним перегруженным accept
, но тогда вам нужно будет передать "тег посещения" посетителю, чтобы определить, какой вид посещения он ожидает. Это, вероятно, не стоит беспокоиться.
Вторая проблема заключается в том, что std::function
не поддерживает множественные перегрузки типов аргументов. Одним из применений посетителя является то, что мы отправляем по-разному в зависимости от динамического типа элемента - полная двойная отправка.
В качестве конкретного примера представим 3 вида посещения: сохранение, загрузка и отображение. Основное различие между сохранением и отображением состоит в том, что отображение отбрасывает то, что не видно (либо скрыто, либо установлено как невидимое).
В традиционном элементе / посетителе у вас будет одна функция принятия с 3 перегрузками, каждая из которых принимает Saver*
или Loader*
или Displayer*
, Каждый из Saver
Loader
а также Displayer
имеет кучу visit(element*)
а также visit(derived_element_type*)
методы.
Под std::function
посещение, ваш элемент вместо этого имеет save(std::function<void(element*)>
и load(
и display(
метод. Двойная отправка не производится, потому что std::function
только выставляет один интерфейс.
Теперь мы можем написать std::function
механизм многократной перегрузки, если он нам нужен. Это продвинутый C++, однако.
template<class Is, size_t I>
struct add;
template<class Is, size_t I>
using add_t=typename add<Is,I>::type;
template<size_t...Is, size_t I>
struct add<std::index_sequence<Is...>, I>{
using type=std::index_sequence<(I+Is)...>;
};
template<template<class...>class Z, class Is, class...Ts>
struct partial_apply;
template<template<class...>class Z, class Is, class...Ts>
using partial_apply_t=typename partial_apply<Z,Is,Ts...>::type;
template<template<class...>class Z, size_t...Is, class...Ts>
struct partial_apply<Z,std::index_sequence<Is...>, Ts...> {
using tup = std::tuple<Ts...>;
template<size_t I> using e = std::tuple_element_t<I, tup>;
using type=Z< e<Is>... >;
};
template<template<class...>class Z, class...Ts>
struct split {
using left = partial_apply_t<Z, std::make_index_sequence<sizeof...(Ts)/2>, Ts...>;
using right = partial_apply_t<Z, add_t<
std::make_index_sequence<(1+sizeof...(Ts))/2>,
sizeof...(Ts)/2
>, Ts...>;
};
template<template<class...>class Z, class...Ts>
using right=typename split<Z,Ts...>::right;
template<template<class...>class Z, class...Ts>
using left=typename split<Z,Ts...>::left;
template<class...Sigs>
struct functions_impl;
template<class...Sigs>
using functions = typename functions_impl<Sigs...>::type;
template<class...Sigs>
struct functions_impl:
left<functions, Sigs...>,
right<functions, Sigs...>
{
using type=functions_impl;
using A = left<functions, Sigs...>;
using B = right<functions, Sigs...>;
using A::operator();
using B::operator();
template<class F>
functions_impl(F&& f):
A(f),
B(std::forward<F>(f))
{}
};
template<class Sig>
struct functions_impl<Sig> {
using type=std::function<Sig>;
};
который дает вам std::function
который поддерживает несколько подписей (но только одну функцию). Чтобы использовать это, попробуйте что-то вроде:
functions< void(int), void(double) > f = [](auto&& x){std::cout << x << '\n'; };
который при вызове с int
, печатает int, и когда вызывается с double
печатает двойной.
Как уже отмечалось, это продвинутый C++: я просто включил его, чтобы отметить, что язык достаточно мощный, чтобы справиться с этой проблемой.
С помощью этой техники вы можете выполнить двойную отправку, используя std::function
Тип интерфейса. Ваш простой посетитель должен передать вызываемый объект, который может обрабатывать каждую перегрузку, которую вы отправляете, а ваш элемент должен детализировать все типы, которые он ожидает от посетителя в его поддержке. functions
подпись.
Вы заметите, что если вы реализуете это, вы получите действительно волшебный полиморфизм при посещении. Вы будете вызываться со статическим типом объекта, который вы посещаете, динамически, и вам нужно будет написать только одно тело метода. Добавление новых требований к контракту происходит в одном месте (при объявлении интерфейса метода accept) вместо 2+K, как при классическом посещении (в методе accept, в интерфейсе типа посещения и в каждой из различных перегрузок). класса посещения (который может быть устранен с помощью CRTP, я признаю).
Выше functions<Sigs...>
хранит N копий функции. Более оптимальным является сохранение функции tur один раз и N представлений вызова. Это прикосновение сложнее, но только прикосновение.
template<class...Sigs>
struct efficient_storage_functions:
functions<Sigs...>
{
std::unique_ptr<void, void(*)(void*)> storage;
template<class F> // insert SFINAE here
efficient_storage_functions(F&& f):
storage{
new std::decay_T<F>(std::forward<F>(f)),
[](void* ptr){
delete static_cast<std::decay_t<F>*>(ptr);
}
},
functions<Sigs...>(
std::reference_wrapper<std::decay_t<F>>(
get<std::decay_t<F>>()
)
)
{}
template<class F>
F& get() {
return *static_cast<F*>(storage.get());
}
template<class F>
F const& get() const {
return *static_cast<F const*>(storage.get());
}
};
следующее необходимо улучшить с помощью оптимизации небольших объектов (чтобы не хранить тип в стеке) и поддержки SFINAE, чтобы он не пытался создавать из несовместимых вещей.
Он хранит одну копию входящего вызова в unique_ptr
и множество std::function
s это наследует от всего магазина std::reverence_wrapper
к его содержанию.
Также отсутствует копия-конструкция.
Ваша идея предоставления std::function
Для вашего посетителя на стройке стоит задача двойной отправки: посетитель должен реализовать функцию просмотра для каждого конкретного типа объекта, который он может посетить.
Вполне возможно, что вы можете предоставить один std::function
которая отвечает этой задаче (например, все конкретные элементы являются производными одного базового класса). Но это не всегда возможно.
Кроме того, посетитель не обязательно без гражданства. Он может поддерживать состояние для каждой структуры, которую он посещает (пример: поддержание количества элементов или общей суммы). Хотя это легко кодировать на уровне класса посетителя, в std::function
, Это означает, что ваша реализация посетителя будет иметь некоторые ограничения в его возможном использовании.
Поэтому я бы порекомендовал поработать с производным классом посетителей: он более читабелен, работает, даже если конкретные элементы не связаны, и дает вам больше гибкости, например, для посетителей с отслеживанием состояния.
(В этом другом ответе вы можете найти наивный пример абстрактного посетителя с полученным конкретным посетителем с отслеживанием состояния, работающим с несвязанными конкретными элементами)