11.5 Symmetry with the Brute-Force Dispatcher
When you hatch the intersection between two shapes, you might want to do it differently if you have a rectangle covering an ellipse than if you have an ellipse covering a rectangle. Or, on the contrary, you might need to hatch the intersection area the same way when an ellipse and a rectangle intersect, no matter which covers which. In the latter case, you need a symmetric multimethod—a multimethod that is insensitive to the order in which you pass its arguments.
Symmetry applies only when the two parameter types are identical (in our case, BaseLhs is the same as BaseRhs, and LhsTypes is the same as RhsTypes).
The brute-force StaticDispatcher defined previously is asymmetric; that is, it doesn't offer any built-in support for symmetric multimethods. For example, assume you define the following classes:
class HatchingExecutor
{
public:
void Fire(Rectangle&, Rectangle&);
void Fire(Rectangle&, Ellipse&);
...
// Error handler
void OnError(Shape&, Shape&);
};
typedef StaticDispatcher
<
HatchingExecutor,
Shape,
TYPELIST_3(Rectangle, Ellipse, Poly)
>
HatchingDispatcher;
The HatchingDispatcher does not fire when passed an Ellipse as the left-hand parameter and a Rectangle as the right-hand parameter. Even though from your HatchingExecutor's viewpoint it doesn't matter who's first and who's second, HatchingDispatcher will insist that you pass objects in a certain order.
We can fix the symmetry in the client code by reversing arguments and forwarding from one overload to another:
class HatchingExecutor
{
public:
void Fire(Rectangle&, Ellipse&);
// Symmetry assurance
void Fire(Ellipse& lhs, Rectangle& rhs)
{
// Forward to Fire(Rectangle&, Ellipse&)
// by switching the order of arguments
Fire(rhs, lhs);
}
...
};
These little forwarding functions are hard to maintain. Ideally, StaticDispatcher would provide itself optional support for symmetry through an additional bool template parameter, which is worth looking into.
The need is to have StaticDispatcher reverse the order of arguments when invoking the callback, for certain cases. What are those cases? Let's analyze the previous example. Expanding the template argument lists from their default values, we get the following instantiation:
typedef StaticDispatcher
<
HatchingExecutor,
Shape,
TYPELIST_2(Rectangle, Ellipse, Poly), // TypesLhs
Shape,
TYPELIST_2(Rectangle, Ellipse, Poly), // TypesRhs
void
>
HatchingDispatcher;
An algorithm for selecting parameter pairs for a symmetric dispatcher can be as follows: Combine the first type in the first typelist (TypesLhs) with each type in the second typelist (TypesRhs). This gives three combinations: Rectangle-Rectangle, Rectangle-Ellipse, and Rectangle-Poly. Next, combine the second type in Types Lhs (Ellipse) with types in TypesRhs. However, because the first combination (Rectangle-Ellipse) has already been made in the first step, this time start with the second element in Types Rhs. This step yields Ellipse-Ellipse and Ellipse-Poly. The same reasoning applies to the next step: Poly in TypesLhs must be combined only with types starting with the third one in TypesRhs. This gives only one combination, Poly-Poly, and the algorithm stops here.
Following this algorithm, you implement only the functions for the selected combination, as follows:
class HatchingExecutor
{
public:
void Fire(Rectangle&, Rectangle&);
void Fire(Rectangle&, Ellipse&);
void Fire(Rectangle&, Poly&);
void Fire(Ellipse&, Ellipse&);
void Fire(Ellipse&, Poly&);
void Fire(Poly&, Poly&);
// Error handler
void OnError(Shape&, Shape&);
};
StaticDispatcher must detect all by itself the combinations that were eliminated by the algorithm just discussed, namely Ellipse-Rectangle, Poly-Rectangle, and Poly-Ellipse. For these three combinations, StaticDispatcher must reverse the arguments. For all others, StaticDispatcher forwards the call just as it did before.
What's the Boolean condition that determines whether or not argument swapping is needed? The algorithm selects the types in TL2 only at indices greater than or equal to the index of the type in TL1. Therefore, the condition is as follows:
For two types T and U, if the index of U in TypesRhs is less than the index of T in TypesLhs, then the arguments must be swapped.
For example, say T is Ellipse and U is Rectangle. Then T's index in TypesLhs is 1 and U's index in TypesRhs is 0. Consequently, Ellipse and Rectangle must be swapped before invoking Executor::Fire, which is correct.
The typelist facility already provides the IndexOf compile-time algorithm that returns the position of a type in a typelist. We can then write the swapping condition easily.
First, we must add a new template parameter that says whether the dispatcher is symmetric. Then, we add a simple little traits class template, InvocationTraits, which either swaps the arguments or does not swap them when calling the Executor::Fire member function. Here is the relevant excerpt.
template
<
class Executor,
bool symmetric,
class BaseLhs,
class TypesLhs,
class BaseRhs = BaseLhs,
class TypesRhs = TypesLhs,
typename ResultType = void
>
class StaticDispatcher
{
template <bool swapArgs, class SomeLhs, class SomeRhs>
struct InvocationTraits
{
static void DoDispatch(SomeLhs& lhs, SomeRhs& rhs,
Executor& exec)
{
exec.Fire(lhs, rhs);
}
};
template <class SomeLhs, class SomeRhs>
struct InvocationTraits<True, SomeLhs, SomeRhs>
{
static void DoDispatch(SomeLhs& lhs, SomeRhs& rhs,
Executor& exec)
{
exec.Fire(rhs, lhs); // swap arguments
}
}
public:
static void DispatchRhs(BaseLhs& lhs, BaseRhs& rhs,
Executor exec)
{
if (Head* p2 = dynamic_cast<Head*>(&rhs))
{
enum { swapArgs = symmetric &&
IndexOf<Head, TypesRhs>::result <
IndexOf<BaseLhs, TypesLhs>::result };
typedef InvocationTraits<swapArgs, BaseLhs, Head>
CallTraits;
return CallTraits::DoDispatch(lhs, *p2);
}
else
{
return StaticDispatcher<Executor, BaseLhs,
NullType, BaseRhs, Tail>::DispatchRhs(
lhs, rhs, exec);
}
}
};
Support for symmetry adds some complexity to StaticDispatcher, but it certainly makes things much easier for StaticDispatcher's user.
|