Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Would Single Dispatch be a good start for C++17?

228 views
Skip to first unread message

DeMarcus

unread,
May 4, 2013, 8:36:36 AM5/4/13
to
Hi,

The following is a quite common scenario.

int main()
{
std::vector<Shape*> shapeVector;
// ...

for( auto& shape : shapeVector )
{
streamShape( shape, std::cout );
}

return 0;
}

In order to make that happen in a decoupled way we must add quite a lot
of boiler plate code, usually by means of the Visitor pattern. There are
at least two major problems with the Visitor pattern though:

* Every class that needs to be streamed must have an accept() function
which is intrusive but even more disturbing boiler plate code.

* It's not trivial to provide arguments to that accept() function since
it has to be general for any kind of Visitor where all visitors use
their own arguments. The StreamVisitor above needs the stream as a
parameter while a DrawVisitor might need the memory to draw to.

How do you provide Visitor arguments in your code?


There have been ideas to solve this situation where Stroustrup and his
team provided the following solution.

http://www.stroustrup.com/multimethods.pdf

If we look back at my original problem above it could be solved with
Stroustrup's technique the following way. Note the /virtual/ keyword in
the argument list which is their way of saying that the function should
be resolved in run-time.

void streamShape( virtual Circle* circle, std::ostream& os )
{
// ...
}

void streamShape( virtual Rectangle* rectangle, std::ostream& os )
{
// ...
}

int main()
{
std::vector<Shape*> shapeVector;
// ...

for( auto& shape : shapeVector )
{
streamShape( shape, std::cout );
}

return 0;
}


With Stroustrup et. al.'s simple technique we solve all the
uncomfortable problems with the Visitor pattern. The solution that they
provide solves more than just the Visitor pattern though, hence it also
implies quite a restructuring of the virtual table.

However, correct me if I'm wrong now, but what we do above is to use
only one virtual argument, so we only use /Single/ Dispatch. Single
Dispatch we already have in C++ with the normal virtual methods so in
order to implement Single Dispatch also for free functions as we do
above, we could simply use the virtual table we already have.


I see that Single Dispatch for free functions could be a good start to
introduce Stroustrup et. al.'s Multimethods technique. What do you think?


Regards,
Daniel


--
[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]

Dave Harris

unread,
May 5, 2013, 1:02:05 PM5/5/13
to
In article <5184f430$0$32116$1472...@news.sunsite.dk>,
demarcus_at...@tellus.orb.dotsrc.org (DeMarcus) wrote:
> * Every class that needs to be streamed must have an accept()
> function which is intrusive but even more disturbing boiler plate
> code.

An alternative is to use dynamic_cast.

struct Visitor {
void visit( Shape *pShape ) {
if (Rectangle *pRect = dynamic_cast<Rectangle *>(pShape))
visit( *pRect );
else //... other shapes.
}
virtual void visit( Rectangle &rect ) = 0;
// ...
};

I try to avoid this because with my compiler, a dynamic_cast is a
lot slower than a virtual function call. However, some of my
colleagues prefer it because it's more flexible and less intrusive.
You can, for example, provide a syntax like:

visitor.registerReciever<Rectangle>();
visitor.registerReciever<Circle>();

that uses templates, dynamic_cast and overloading to dispatch only
to classes of interest.


> * It's not trivial to provide arguments to that accept() function
> since it has to be general for any kind of Visitor where all
> visitors use their own arguments. The StreamVisitor above needs
> the stream as a parameter while a DrawVisitor might need the memory
> to draw to.
>
> How do you provide Visitor arguments in your code?

In C++03, make them member variables of the visitor class. In C++11,
you could use lambdas instead.

Visitor visitor;
visitor.on<Rectangle>( [&]{ Rectangle &r ) {
os << r;
} );
visitor.on<Circle>( [&]{ Circle &c ) {
os << c;
} );

for (auto& shape : shapeVector)
visitor.visit( shape );

> However, correct me if I'm wrong now, but what we do above is to
> use only one virtual argument, so we only use /Single/ Dispatch.
> Single Dispatch we already have in C++ with the normal virtual
> methods so in order to implement Single Dispatch also for free
> functions as we do above, we could simply use the virtual table we
> already have.

Single dispatch is certainly simpler. However, with dynamic
linking we don't know until runtime how many slots we need in
the vtable, so we can't just use the vtable we already have.


> I see that Single Dispatch for free functions could be a good start
> to introduce Stroustrup et. al.'s Multimethods technique. What do
> you think?

It's probably better to have one language change than two.

-- Dave Harris, Nottingham, UK.

Öö Tiib

unread,
May 6, 2013, 12:57:12 AM5/6/13
to
On Saturday, 4 May 2013 15:36:36 UTC+3, DeMarcus wrote:
> Hi,
>
> The following is a quite common scenario.
>
> int main()
> {
>
> std::vector<Shape*> shapeVector;
> // ...
>
> for( auto& shape : shapeVector )
> {
> streamShape( shape, std::cout );
> }
>
> return 0;
> }
>
> In order to make that happen in a decoupled way we must add quite a
> lot of boiler plate code, usually by means of the Visitor pattern.

Visitors are usually used for a group of objects (usually a tree) that
do not belong to single hierarchy of inheritance; the shapes above
seem to be in a vector and have all common base class. IOW Visitor
seems to be overkill here. Why not to use some virtual function of
'Shape'?

> There are at least two major problems with the Visitor pattern
> though:
>
> * Every class that needs to be streamed must have an accept()
> function which is intrusive but even more disturbing boiler plate
> code.

That is not a big problem. That 'accept' is short and trivial 3-liner.

> * It's not trivial to provide arguments to that accept() function
> since it has to be general for any kind of Visitor where all
> visitors use their own arguments. The StreamVisitor above needs the
> stream as a parameter while a DrawVisitor might need the memory to
> draw to.

Visitors that I have seen have state. It is not business of objects
being visited to affect to what stream that visitor streams them.

The real problem wit Visitors is that when the classes visited need to
be reorganized somehow (merged, split, removed, subclassed or added)
then all the Visitors have to be maintained and retested. That makes
it more expensive to extend and to maintain. Visitor is therefore evil
and so should be used when it is least evil.

> How do you provide Visitor arguments in your code?

I provide them in constructor of visitor. Some printing visitor does
not switch to other printer during its visit to some tree of objects
that it has to print.

> There have been ideas to solve this situation where Stroustrup and
> his team provided the following solution.
>
> http://www.stroustrup.com/multimethods.pdf

Lot of people have difficulties with virtual functions. These
multimethods seem to square the complexity of virtual functions. It
is not a tool for average programmer i'm afraid.

> If we look back at my original problem above it could be solved with
> Stroustrup's technique the following way. Note the /virtual/ keyword
> in the argument list which is their way of saying that the function
> should be resolved in run-time.
>
> void streamShape( virtual Circle* circle, std::ostream& os )
> {
> // ...
> }
>
> void streamShape( virtual Rectangle* rectangle, std::ostream& os )
> {
> // ...
> }

Those are not multimethods. The whole point of a multimethod is to
have multiple virtual parameters.

> With Stroustrup et. al.'s simple technique we solve all the
> uncomfortable problems with the Visitor pattern. The solution that
> they provide solves more than just the Visitor pattern though, hence
> it also implies quite a restructuring of the virtual table.
>
> However, correct me if I'm wrong now, but what we do above is to use
> only one virtual argument, so we only use /Single/ Dispatch. Single
> Dispatch we already have in C++ with the normal virtual methods so
> in order to implement Single Dispatch also for free functions as we
> do above, we could simply use the virtual table we already have.

Yes. Exactly. We should always aim to use simplest solution that
solves the problem (but not simpler).

> I see that Single Dispatch for free functions could be a good start
> to introduce Stroustrup et. al.'s Multimethods technique. What do
> you think?

I think that most understand it better if we just use the already
existing single dispatch with already existing syntax:

void Circle::streamTo( std::ostream&) const
{
// ...
}

void Rectangle::streamTo( std::ostream&) const
{
// ...
}

int main()
{
std::vector<Shape*> shapeVector;
// ...

for( auto& shape : shapeVector )
{
shape.streamTo( std::cout );
}
return 0;
}

If you need a free function (why?) then you can make it to call that
virtual 'streamTo' and done.
0 new messages