将 std::function 应用于访问者设计模式
applying std::function to visitor design pattern
我对 ood 有点陌生。阅读 GoF 的设计模式我发现了 Visitor。
我的访客模式版本比 Generic visitor using variadic templates 中提到的更具体。因此,我的想法是通过在构建期间提供私有 std::function
s 来创建具体的访问者。然后,每个访问函数都会调用相应的private std::function
。
我的问题:如上所述实施访问者是否是一种好的做法,如果不是,为什么?
唯一想到的缺点是歧义,也就是说,很难知道访问者的特定实例将在组合上做什么。
使用 std::function
访问者实现访问者的方式是更改元素的接受部分。你失去了双分派作为成本,但你确实抽象了一点迭代的样板。
代替元素上的一个 accept
方法,每个 种类 访问有一个 accept
。
当您想在标准访问者中以不止一种方式访问事物时,您可以编写更多访问者类型,并添加新的 accept
重载来接受它们。
在基于 std::function
的函数中,您只需编写一个新的 accept
类型的函数 并使用不同的名称;该名称位于方法的名称中,而不是访问者类型的名称中(因为访问者类型是匿名的)。
在具有 SFINAE std::function
smarts 的 C++14 中,您可以使用一个重载 accept
,但是您必须向访问者传递一个 'visit tag'确定它期望什么样的访问。这可能不值得麻烦。
第二个问题是std::function
不支持参数类型的多重重载。 visitor的用途之一就是我们根据元素的动态类型进行不同的调度——full double dispatch.
作为一个具体的案例研究,想象一下3种访问:保存、加载和显示。保存和显示之间的主要区别是显示剔除不可见的东西(被遮挡或设置为不可见)。
在传统的 element/visitor 下,您将有一个带有 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
调用时,打印一个 double。
如前所述,这是高级 C++:我只是将其包括在内是为了说明该语言功能强大,足以处理该问题。
使用该技术,您可以使用 std::function
类型的接口进行双重分派。您的简单访问者必须传入一个可调用对象来处理您分派的每个重载,并且您的元素必须详细说明它希望访问者能够在其 functions
签名中支持的所有类型。
您会注意到,如果您实现它,您将在访问的视线中获得一些非常神奇的多态性。将使用您正在访问的事物的 静态类型 动态调用您,您只需编写一个方法体。向合约添加新需求发生在一个地方(在 accept 方法的接口声明处),而不是像 classic 访问那样的 2+K(在 accept 方法中,在访问类型的接口中,在访问的各种重载中的每一个 class(我承认可以用 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::reverence_wrapper
中继承的无数 std::function
到它的内容。
还缺少复制构造。
您在构造时为 visitor 提供 std::function
的想法面临双重分派的挑战:访问者必须为其可能访问的每个具体对象类型实现一个访问函数。
您可以提供一个 std::function
来应对这一挑战(例如:所有具体元素都是同一基础 class 的派生物)。但这并不总是可能的。
另外,访问者不一定是无国籍的。它可以为其访问的每个结构维护状态(例如:维护元素计数或总计)。虽然这在访问者 class 级别很容易编写代码,但在 std::function
级别则更难。这意味着您的访问者实现在其可能的用途上会有一些限制。
因此,我宁愿推荐使用派生访问者 class:这更具可读性,即使具体元素不相关也能工作,并为您提供更大的灵活性,例如有状态访问者。
(在此 other answer 中,您可以找到抽象访问者的简单示例,其中派生的具体状态访问者使用不相关的具体元素)
我对 ood 有点陌生。阅读 GoF 的设计模式我发现了 Visitor。
我的访客模式版本比 Generic visitor using variadic templates 中提到的更具体。因此,我的想法是通过在构建期间提供私有 std::function
s 来创建具体的访问者。然后,每个访问函数都会调用相应的private std::function
。
我的问题:如上所述实施访问者是否是一种好的做法,如果不是,为什么?
唯一想到的缺点是歧义,也就是说,很难知道访问者的特定实例将在组合上做什么。
使用 std::function
访问者实现访问者的方式是更改元素的接受部分。你失去了双分派作为成本,但你确实抽象了一点迭代的样板。
代替元素上的一个 accept
方法,每个 种类 访问有一个 accept
。
当您想在标准访问者中以不止一种方式访问事物时,您可以编写更多访问者类型,并添加新的 accept
重载来接受它们。
在基于 std::function
的函数中,您只需编写一个新的 accept
类型的函数 并使用不同的名称;该名称位于方法的名称中,而不是访问者类型的名称中(因为访问者类型是匿名的)。
在具有 SFINAE std::function
smarts 的 C++14 中,您可以使用一个重载 accept
,但是您必须向访问者传递一个 'visit tag'确定它期望什么样的访问。这可能不值得麻烦。
第二个问题是std::function
不支持参数类型的多重重载。 visitor的用途之一就是我们根据元素的动态类型进行不同的调度——full double dispatch.
作为一个具体的案例研究,想象一下3种访问:保存、加载和显示。保存和显示之间的主要区别是显示剔除不可见的东西(被遮挡或设置为不可见)。
在传统的 element/visitor 下,您将有一个带有 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
调用时,打印一个 double。
如前所述,这是高级 C++:我只是将其包括在内是为了说明该语言功能强大,足以处理该问题。
使用该技术,您可以使用 std::function
类型的接口进行双重分派。您的简单访问者必须传入一个可调用对象来处理您分派的每个重载,并且您的元素必须详细说明它希望访问者能够在其 functions
签名中支持的所有类型。
您会注意到,如果您实现它,您将在访问的视线中获得一些非常神奇的多态性。将使用您正在访问的事物的 静态类型 动态调用您,您只需编写一个方法体。向合约添加新需求发生在一个地方(在 accept 方法的接口声明处),而不是像 classic 访问那样的 2+K(在 accept 方法中,在访问类型的接口中,在访问的各种重载中的每一个 class(我承认可以用 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::reverence_wrapper
中继承的无数 std::function
到它的内容。
还缺少复制构造。
您在构造时为 visitor 提供 std::function
的想法面临双重分派的挑战:访问者必须为其可能访问的每个具体对象类型实现一个访问函数。
您可以提供一个 std::function
来应对这一挑战(例如:所有具体元素都是同一基础 class 的派生物)。但这并不总是可能的。
另外,访问者不一定是无国籍的。它可以为其访问的每个结构维护状态(例如:维护元素计数或总计)。虽然这在访问者 class 级别很容易编写代码,但在 std::function
级别则更难。这意味着您的访问者实现在其可能的用途上会有一些限制。
因此,我宁愿推荐使用派生访问者 class:这更具可读性,即使具体元素不相关也能工作,并为您提供更大的灵活性,例如有状态访问者。
(在此 other answer 中,您可以找到抽象访问者的简单示例,其中派生的具体状态访问者使用不相关的具体元素)