Rocksolid Light

Welcome to novaBBS (click a section below)

mail  files  register  newsreader  groups  login

Message-ID:  

The road to hell is paved with NAND gates. -- J. Gooding


devel / comp.lang.c++ / Re: What is visitor pattern?

Re: What is visitor pattern?

<G1CFM.897823$GMN3.400773@fx16.iad>

  copy mid

https://www.novabbs.com/devel/article-flat.php?id=1142&group=comp.lang.c%2B%2B#1142

  copy link   Newsgroups: comp.lang.c++
Path: i2pn2.org!i2pn.org!usenet.blueworldhosting.com!diablo1.usenet.blueworldhosting.com!peer01.iad!feed-me.highwinds-media.com!news.highwinds-media.com!fx16.iad.POSTED!not-for-mail
Subject: Re: What is visitor pattern?
Newsgroups: comp.lang.c++
References: <331cb164-e6b3-4f9b-af8d-ee933abd79b7n@googlegroups.com>
<uIsEM.123835$QQFb.6082@fx38.iad>
<6df46174-6867-4577-a73d-0fb70d6a25e8n@googlegroups.com>
<9ea1f202-4524-03b8-f003-e11b7fa411e7@removeyourself.dontspam.yahoo>
<c772c32e-d0e0-4199-b173-c64e3535159cn@googlegroups.com>
<msVEM.440194$xMqa.182640@fx12.iad>
<942afb61-7d90-4251-87a3-18cf4c68fa03n@googlegroups.com>
From: pauldont...@removeyourself.dontspam.yahoo (Pavel)
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101
Firefox/91.0 SeaMonkey/2.53.17
MIME-Version: 1.0
In-Reply-To: <942afb61-7d90-4251-87a3-18cf4c68fa03n@googlegroups.com>
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 8bit
Lines: 301
Message-ID: <G1CFM.897823$GMN3.400773@fx16.iad>
X-Complaints-To: https://www.astraweb.com/aup
NNTP-Posting-Date: Thu, 24 Aug 2023 05:38:46 UTC
Date: Thu, 24 Aug 2023 01:38:27 -0400
X-Received-Bytes: 15809
 by: Pavel - Thu, 24 Aug 2023 05:38 UTC

Öö Tiib wrote:
> On Tuesday, 22 August 2023 at 05:55:32 UTC+3, Pavel wrote:
>> Öö Tiib wrote:
>>> On Monday, 21 August 2023 at 01:02:21 UTC+3, Pavel wrote:
>>>> Öö Tiib wrote:
>>>>> On Sunday, 20 August 2023 at 21:13:01 UTC+3, Pavel wrote:
>>>>>
>>>>>> 1. Ensure all objects have a common base class (say, it's B)
>>>>>
>>>>> Not needed.
>>>>>
>>>>>> 2. create an abstract class Visitor with an abstract method doOp with an
>>>>>> arg of type [ref to] B.
>>>>>
>>>>> Nope, pile of overloads of doOp one for each type processed.
>>>>
>>>> This would render the pattern useless as has no advantage over adding a
>>>> virtual method to do the op. The rationale of the pattern is to permit
>>>> coding one op in one place.
>>>>
>>> The Visitor class is that "one place"; doing same operation lets say printing
>>> of 50 types in one function results with huge function that is headache
>>> not advantage. Separate functions are testable and maintainable.
>>>
>>>>> Otherwise
>>>>> how you process different types differently?
>>>> I explained this in the problem statement. "Given [a ref to] any such
>>>> object, you are able to
>>>> compute its type and/or call a virtual method".
>>>
>>> IOW you do huge switch typeid ... case static_ cast or have the virtual
>>> method (over what we were "gaining advantage") already there?
>>
>> Even a switch with 30 cases is no worse than 30 methods although switch
>> is not always necessary (try to read the above instead of imagining).
>>
> Visitor is typically used to search, filter, draw or print whole data object
> hierarchy, convert to JSON to XML or to tree in GUI.
correct

> If whole data hierarchy
> is small then you get 30 cases. Switch case is worse than virtual methods.
*In general* neither is better or worse. In particular, as I explained
at the beginning, if the operation is decomposed correctly, its behavior
on the objects of different types has to be coherent, similar in some
way (and maybe more similar than different) and, as such, a significant
parts of the context or results of intermediate computations could be
shared. I demonstrated this in my example with red and green borders: a)
a condition by area prevents visitor from operating on objects is
computed from their property that is orthogonal to their dynamic type;
and b) the exact behavior of the operation (red vs green border in the
example) depends on a subset of types (which, in general, is not
necessarily compact in the hierarchy).

> Think why virtual methods were added? To get rid of switch cases over type.
Of course, not. Virtual methods were *used*, with less or greater
success, to replace switch statements in event-processing scenarios
(like window behavior) (with greater success) or in state machines (with
lesser success).

But, they were *invented* to implement polymorphic behavior and
relatively loose coupling (generally, lesser dependencies) between the
code that calls the method and the code that implements the method.

Obviously, a single switch statement couples everything together and the
compilation unit where the switch is depends on every piece of code and
every concrete type the switch statement handles -- to the extent its
behavior in the operation must differ from that of other types). But
this loose coupling is only desirable when the behaviors are severely
different. On the other hand, when the operation does *to significant
extent, a single thing* on different type, with 2-3 relatively small
type-dependent differences, these differences are often better expressed
in if statement, switch or in a number of different ways (including
calling combinations of virtual functions *from within the top-level
method implementing the operation*)

> Visitor is not meant as excuse to add those back, otherwise just use
> the switch case in function, do not manufacture visitors that do not use
> double dispatch for confusion.
Ohh. Not again. If you want to use virtual functions, use them, you
don't need visitor for this. Add virtual void newOperation(context
parameters) to the base class and call it in some for_each. This is
exactly what I explained in the first post: Visitor itself is an
alternative to adding a virtual methods. The criteria which one to
choose are well known from the original works on Visitor, like GoF etc:
if your type hierarchy is big, stable, its model is well understood and
established (like GUI toolkit), and hence you are unlikely to change the
hierarchy often BUT you need to add new operations often, use visitors
(because you don't need to add thing to every class for every
operation). If your type hierarchy is dynamic and is expected to grow
often but the set of operation is stable, well establish, unlikely to
change often (like unit actions in a strategy game where you add and
enhance units but all they are going to do is to essentially move, shoot
and die), add a virtual method.

The first thing to explain about Visitor (or any other design pattern)
is how to decide when to use it and when not to use it (the scope).

If the decision to use Visitor is correct, we already know the class
hierarchy is stable and there is no harm for the code to depend on a
fixed set of these classes. The double dispatch for Visitor is just a
tool to dispatch to the code specific to the type after you dispatched
to the specific operation. Nothing more, nothing less, see definition of
multiple dispatch. The specific way to double-dispatch that many Visitor
examples (unfortunately, started with GoF book, and shown in Wikipedia)
show, is simply the *implementation detail*. You do not have to trust me.

Says Wikipedia:

"In software engineering, double dispatch is a special form of multiple
dispatch, and a mechanism that dispatches a function call to different
concrete functions depending on the runtime types of two objects
involved in the call"

And, looking at same Wikipedia on multiple dispatch, C++ is *not* one of
the languages that natively support multiple dispatch and there are
multiple ways of implementing it, among them
- "using run time type comparison via dynamic_cast" (if statement method)
- "pointer-to-method lookup table"; and (misleadingly)
- "the visitor pattern"

Why "misleadingly"? Because, looking at visitor pattern, you find

"The visitor takes the instance reference as input, and implements the
goal through double dispatch."

So, according to Wikipedia article on multiple dispatch one can
implement it (among the others) using visitor pattern, whereas visitor
pattern "implements the goal via double dispatch" whereas the double
dispatch is a special form of multiple dispatch.

Some readers (which seem to include you) resolve this circular reference
ad-hoc: they see that *specific implementation* of multiple dispatch in
C++ that happens to illustrate the Visitor article (stemming from the
GoF book, unfortunately) and somehow decide that this is *the right way*
(TM) to implement double dispatch. This is despite:

- that double dispatch is defined as a concept an there is no *the right
way* of doing it in C++ and Wikipedia itself illustrates two such ways
(both different from the one used in the visitor article and GoF book)
in the article on multiple dispatch.

- that it must be obvious that it is Visitor that uses double dispatch
in its implementation, not other may around, because the part cannot use
the whole and Visitor is "bigger" than double-dispatch because it uses
both double dispatch and iteration (through the objects in a data
structure) but iteration is not a part of double dispatch.

The problem with that (by and large, randomly-selected) implementation
of double dispatch is that it is a bad choice to illustrate Visitor
pattern specifically. Besides the mere distracting the reader to its
clever *implementation details* (not essential for understanding the
Visitor pattern), it also makes it difficult to emphasize the coherence
and cohesiveness of the newly added operation and does nothing to
explain how to organize the access of the new operation to all pieces of
data it may need. Taking into account that the whole Visitor pattern is
about adding a new operation to working on a collection or stream ("the
structure") of objects, it is a very unfortunate choice indeed.

>
>> The advantage is in having this logic in one place. If your visitor does
>> a coherent thing (which it is supposed to do; else why is it a single
>> visitor, there could be and usually would be common code for all or many
>> visited). The most pliant cases (that are exactly those gaining most
>> from applying a visitor pattern) may get by object virtual methods.
>> Simple example:
>>
>> If we wanted a green border around a shape of any of 10 classes derived
>> from convex polygon (ConvexTriangle, ConvexQuadrilateral etc), red
>> border around a shape of any of 10 classes derived from Oval and no
>> border around any of 30 other zero-area shape classes, all we need to do
>> is to change doOp to:
>>
>
> Unused cp in your code?
Yes, I started with taking bounding rectangle from every type separately
(which sometimes may be a valid micro-optimization if we use a fully
qualified non-virtual call; but then decided to keep it simple to
emphasize the points I wanted to emphasize)

> Can't post sane code from gg ... but also don't want to install newsreaders
> to all devices I use. It is drawing border to any shape and so it is unclear
> why you need visitor. Logical one place is either DrwawingContext's or
> Shape's nonvirtual method or free function:
>
> void Shape::draw_border(DrawingContext &dc) const {
> if (area() <= 25) return;
> const Rectangle br{boundingRectangle().increaseBy(0.1)};
> if (isConvexPolygon()) { dc.drawGreenRectangle(br); return; }
> assert(isOval());
> dc.drawRedRectangle(br);
> }
exactly, if you are ok to change the hierarchy every time you add an op,
you might not need the visitor, all you need is to call draw_border from
some for_each loop or high-order function.

This might be a valid choice sometimes (my first post started with
this), but you pay by changing every class *every time you need a new
operation* and making all Shape classes dependent on DrawingContext (and
other contexts you might need for different operations (e.g. log file,
network socket, database connection etc)). In visitor pattern, on the
other hand, the concrete visitor is the only code that becomes dependent
on that op-specific context.

>
>>
>> And this is the complete code for adding a new operation (virtual cast
>> is to be only defined once for all operations)
>
> You forget that you have BorderingVisitor class of unclear life , inject
> DrawingContext to it (is it copy?), etc. result is
>
> BorderingVisitor bv(dc);
> s.acceptVisit(bv);
>
> instead one of:
>
> dc.draw_border(s);
>
> s.draw_border(dc);
>
> draw_border(dc, s);
>
>>> But we need no bloat from that pattern to do neither. These were the
>>> things we wanted to get rid of.
>> Care to show us the code for your "non bloated" "one-place" visitor
>> class for the above 2 problems? (like I did, you can omit the code that
>> is common for all operations).
>
> Yes, sorry for gg breaking all formating.
>>>
>>>> In practice this can be
>>>> anything, examples (non-exhaustive): a enum type discriminator, virtual
>>>> cast method, (in modern languages), pattern matching.
>>>>>
>>>>>> 3. add a single method to B, e.g. acceptVisit that takes [ref to]
>>>>>> Visitor and calls doOp on it. NOTE: only one such method is needed, no
>>>>>> matter how many ops you are adding.
>>>>>
>>>>> Not enough. Every type processed has to have that copy-paste
>>>>> acceptVisit method. Otherwise double dispatch does not work.
>>>>
>>>> Double dispatch works just fine without copy-pasting. Dispatch #1 (by
>>>> the operation type) is done by the virtual method doOp. Dispatch #2 (by
>>>> the object type) is done by using the actual type to call specific
>>>> methods and/or calling virtual methods, on the object ref.
>>>
>>> Of course huge switch or calling virtual method (or now even multiple)
>>> work without the whole pattern. Read up on "double dispatch".
>>> It is tricky like I already said.
>>>
>> Surely you are the most competent software engineer understanding
>> multiple dispatch. Care to point out a "huge switch" in either of the
>> above 2 examples?
>
> There are no double dispatch in your code so the whole acceptVisit, doOp
> add nothing.
Yes it is, see above (or directly Wikipedia article on multiple dispatch
for the examples on how Double dispatch can be implemented in C++).

> These
Unsure I understand what "these" refers to, will assume my doOp and
acceptVisit functions

> were entirely added to do double dispatch
correct. acceptVisit dispatches by op (the 1st parameter of the outer
structure "visit" operation) and doOp dispatches by object type (in my
examples, either by using its base type's virtual function only or via a
combination of the same and `if' statement on virtual cast result)

> to add
> virtual methods to large potentially unrelated data hierarchies without
> changing any of classes in those hierarchies.
I assume, by "potentially unrelated" data hierarchy, you mean the new op
has to behave entirely differently on every type of the hierarchy and
does not need common data. The issue with this assumption is that the
very first op you add might relate to the classes like this with but the
second might benefit from exploiting the type similarities or grouping
them differently (not even necessary in groups compact in the hierarchy
graph).

> What you do is not even
> use case for visitor pattern.
Says Wikipedia:

"
What problems can the Visitor design pattern solve?

It should be possible to define a new operation for (some) classes of an
object structure without changing the classes.
"

"What I do" achieves the above IMO, so it is definitely the use case.
You might argue whether or not my solution is a Visitor pattern (it is,
at least it includes all canonical Visitor participants (Abstract
Visitor, Concrete Visitors, Abstract Element, Concrete Elements (an
object structure providing access to the elements is implied)) and none
extra) but I don't understand how it can be argued that my code does not
define a new operation for (some) classes of an object structure. And it
surely does not change the classes.

SubjectRepliesAuthor
o What is visitor pattern?

By: wij on Sun, 20 Aug 2023

52wij
server_pubkey.txt

rocksolid light 0.9.81
clearnet tor