访客设计模式和多层类层次结构



我有五个类与相关的访问者:

struct Visitor
{
virtual ~Visitor() = default;
virtual void visit(A&) {}
virtual void visit(B&) {}
virtual void visit(C&) {}
virtual void visit(D&) {}
virtual void visit(E&) {}
};
struct A
{
virtual ~A() = default;
virtual void accept(Visitor& v) { v.visit(*this); }
};
struct B : A { void accept(Visitor& v) override { v.visit(*this); } };
struct C : A { void accept(Visitor& v) override { v.visit(*this); } };
struct D : C { void accept(Visitor& v) override { v.visit(*this); } };
struct E : C { void accept(Visitor& v) override { v.visit(*this); } };

用户代码将在可能的最大抽象级别看到所有实例化,因此它们都将被视为A&。用户代码需要执行两种类型的操作:

  1. 打印"I am C"实例是否完全属于C类型
  2. 打印"I am C"实例是否属于C类型或其任何子类型(即DE(

操作 1 有一个相当容易的实现,并且几乎可以在构建基础设施的情况下脱离访客设计模式框:

struct OperationOne : Visitor
{
void visit( C& ) override { std::cout << "I am C" << std::endl; }
};

正如预期的那样,字符串"I am C"将只打印一次:

int main( )
{
A a; B b; C c; D d; E e;
std::vector<std::reference_wrapper<A>> vec = { a, b, c, d, e };
OperationOne operation_one;
for (A& element : vec)
{
element.accept(operation_one);
}
}

演示

问题是:对于第二个操作,整个基础架构不再工作,假设我们不想重复DE的打印代码:

struct OperationTwo : Visitor
{
void visit( C& ) override { std::cout << "I am C" << std::endl; }
void visit( D& ) override { std::cout << "I am C" << std::endl; }
void visit( E& ) override { std::cout << "I am C" << std::endl; }
};

尽管这很有效,但如果层次结构发生变化并且D不再是C的子类型,而是例如A的直接子类型,则此代码仍然可以编译,但在运行时不会具有预期的行为,这是危险和不可取的。

实现操作 2 的一种解决方案是更改访问者基础结构,以便每个可访问类都将接受的访问者传播到其基类:

struct B : A
{
void accept(Visitor& v) override
{
A::accept( v );
v.visit( *this );
}
};

这样,如果层次结构发生变化,我们将出现编译错误,因为在尝试传播接受的访问者时,编译器将不再找到基类。

也就是说,我们现在可以编写第二个操作 visitor,这次我们不需要复制DE的打印代码:

struct OperationTwo : Visitor
{
void visit(C&) override { std::cout << "I am C" << std::endl; }
}

正如预期的那样,使用OperationTwo时,字符串"I am C"将在用户代码中打印三次:

int main()
{
A a; B b; C c; D d; E e;
vector< reference_wrapper< A > > vec = { a, b, c, d, e };
OperationTwo operation_two;
for ( A& element : vec ) 
{
element.accept( operation_two );
}
}

演示

但是等等:OperationOneOperationTwo代码是完全相同的!这意味着通过更改第二个操作的基础结构,我们基本上破坏了第一个操作。事实上,现在OperationOne也将打印三倍的字符串"I am C"

怎样才能让OperationOneOperationTwo顺利地一起工作?我是否需要将访客设计模式与另一个设计模式相结合,还是根本不需要使用访客?

您可以使用以下内容作为访问者,它将以重载分辨率调度:

template <typename F>
struct OverloadVisitor : Visitor
{
F f;
void visit(A& a) override { f(a); }
void visit(B& b) override { f(b); }
void visit(C& c) override { f(c); }
void visit(D& d) override { f(d); }
void visit(E& e) override { f(e); }
};

然后

struct IAmAC
{
void operator()( C& ) { std::cout << "I am C" << std::endl; }
void operator()( A& ) {} // Fallback
};
using OperationTwo = OverloadVisitor<IAmAC>;

演示

最新更新