我对ood有点新鲜。阅读GoF的设计模式我找到了访客。
我的访客模式版本比Generic visitor using variadic templates中提到的更具体。因此,我的想法是通过在施工期间提供私有std::function
来创建具体的访问者。然后,每个访问函数都会调用相应的私有std::function
。
我的问题:如上所述实施访问者是不错的做法,如果不是,为什么?
只有浮现在脑海中的缺点是含糊不清,也就是说,很难知道访问者的特定实例会对复合做什么。
答案 0 :(得分:3)
使用std::function
访问者实现访问者的方式是更改元素的接受部分。你失去了双重调度作为成本,但你确实抽象了迭代的样板。
而不是元素上的一个accept
方法,每个类访问一个accept
。
如果您希望在标准访问者中以多种方式访问内容,则可以编写更多访问者类型,并添加新的accept
重载以接受它们。
在基于std::function
的版本中,您只需使用不同的名称编写新的accept
类型函数 ;名称位于方法的名称中,而不是访问者类型的名称(因为访问者类型是匿名的)。
在使用SFINAE std::function
智能的C ++ 14中,你可以使用一个超载的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
- esque多调度重载机制。这是高级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
调用时,打印一个double。
如上所述,这是高级C ++:我只是将其包括在内,以指出该语言足以处理该问题。
使用该技术,您可以使用std::function
类型接口进行双重调度。您的访问者必须传递一个可调用的函数,该函数可以处理您发送的每个重载,并且您的元素必须详细说明访问者希望在其functions
签名中支持的所有类型。
你会注意到,如果你实现这一点,你将在访问视线中获得一些非常神奇的多态性。您将动态地使用您正在访问的事物的静态类型调用,并且您只需编写一个方法体。在合同中添加新的需求发生在一个位置(在accept方法的接口声明中),而不是像经典访问那样2 + K(在accept方法中,在访问类型的界面中,以及在每个各种重载中)访问类(可以通过CRTP I&#39; ll承认消除))。
以上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
中存储了无数std::reverence_wrapper
到其内容。
还缺少copy-construct。
答案 1 :(得分:1)
您在构建时向visitor提供std::function
的想法面临双重调度的挑战:访问者必须为其可能访问的每个具体对象类型实现一个visting函数。
您可以提供满足此挑战的单个std::function
(例如:所有具体元素都是同一基类的派生)。但这并不总是可行的。
访客也不一定是无国籍人。它可以维持它访问的evry结构的状态(例如:保持元素数或总数)。虽然这很容易在访问者级别编码,但在std::function
中更难。这意味着您的访问者实现在其可能的用途中会有一些限制。
因此,我建议使用派生的访客类:这更具可读性,即使具体元素不相关也可以使用,并为有状态访问者提供更多灵活性。
(在这个other answer中,您可以找到一个抽象访问者的天真示例,其中派生的具体有状态访问者使用不相关的具体元素)