JohnnyBigert.se
 startsida    |     om mig    |     tjänster    |     gratis C++-tips    |     forskning    |     kontakt     ||   blogg
Smarta
C++-tips
 
 
Behöver ni experthjälp
inom C++?
 
 

(Testa dig själv, svar på fråga 1)
Statisk bindning och designmönstret Visitor

Så här såg programkoden ut i fråga 1:

#include <iostream>

class A {};
class B : public A {};

void bar(const A &) { std::cout << "A"; }
void bar(const B &) { std::cout << "B"; }

int
main()
{
    A a;
    B b;
    A &ref_to_a = a;
    A &ref_to_b = b;

    bar(a);
    bar(b);
    bar(ref_to_a);
    bar(ref_to_b);

    return 0;
}

Kodexempel 1: Testa dig själv! Vad gör programmet?

Frågan var om programkoden kompilerar. Och i så fall, vad händer när man kör programmet?

  • a) Det kompilerar inte.
  • b) Det kompilerar, men det länkar inte.
  • c) Det kompilerar, länkar och skriver ut "ABAB" när det kör.
  • d) Det kompilerar, länkar och skriver ut "ABAA" när det kör.
  • e) Det kompilerar, länkar och skriver ut "AAAA" när det kör.
  • f) Det kompilerar, länkar och skriver ut något annat än (c), (d) och (e).
  • g) Det kompilerar och länkar men kraschar när det kör.

Svaret är: d (markera texten för att se svaret!)

Läs nedan om du vill veta varför!



Funktionsanrop använder statisk uppslagning

Antag att du har en klass A med en virtuell funktion foo. Funktionen foo överskrivs sedan i den nedärvda klassen B (engelska: override). Skriver man då kod av typen B b; A &ref_to_b = b; ref_to_b.foo(); så anropas foo i B. Man får alltså polymorfi tack vare dynamisk uppslagning.

I Kodexempel 1 är situationen en annan. Där har vi en överlagrad funktion bar (dvs flera funktioner med namnet bar men med olika argument/signatur). När vi gör anropet bar(ref_to_b) på sista raden så måste kompilatorn ta reda på vilken av de två varianterna av bar som passar bäst för anropet. Den gör det genom att titta på den statiska typen på argumentet. I detta fall är den statiska typen "referens till A". Det sker alltså ingen dynamisk uppslagning. Så även om det är ett B-objekt som döljer sig bakom A-referensen, så är det bar(const A &) som kommer anropas!

Visitor, när du får oväntat besök

Ett intressant exempel där man använder både dynamisk och statisk uppslagning är designmönstret Visitor (the Visitor pattern, från boken Gamma et al. "Design patterns"). Antag att du håller på att skriva en C++-kompilator. Det första din kompilator gör när den får kod att kompilera är att skapa ett träd som motsvarar C++-grammatiken. De olika elementen i ditt program (tokens, operatorer, variabler, funktioner etc.) representeras av varsin klass. Alla klasser ärver från en interfaceklass TreeNode som definierar användargränssnittet för trädnoder (se artikel om interfaceklasser!). Eftersom C++-grammatiken är rättså omfattande blir det många klasser.

När du väl skapat trädet har du alla möjligheter att göra intressanta analyser av ditt program. Några exempel är syntaxkontroll, semantisk kontroll, statisk kodanalys, kompilering, utskrift av trädet etc. Om man tar utskrift som ett exempel kan man tänka sig att implementera detta som en virtuell funktion print i basklassen TreeNode. Man överskriver sedan print i alla nedärvda klasser. Alla noder i trädet skriver ut sig själva och sina barn. Inga problem där.

För varje ny analys du gör av ditt träd måste du lägga till en ny funktion i alla trädklasser, vilket innebär mycket jobb. Förmodligen leder det till att många filer måste kompileras om, vilket tar tid. Till slut börjar du önska att du slapp ändra alla klasser varje gång du inför en ny analys. Det är här Visitor kommer till undsättning!

Visitor gör din arvshierarki till en klippa

Man använder Visitor-mönstret för att kunna införa ny funktionalitet (nya analyser i vårt exempel) utan att behöva ändra användargränssnittet i en arvshierarki. I kompilatorexemplet innebär det att du kan införa nya analyser utan att ändra dina trädnoder eller basklassen TreeNode! Så hur går det till?

besökare

Vi börjar bakifrån. Vi vill att vår arvshierarki ska förbli stabil och att inget ska ändras. Vi måste därför införa något som löser våra problem en gång för alla. Lösningen måste samtidigt kunna rymma alla framtida typer av analyser på vårt träd. Knepigt fall.

Samarbete mellan statisk och dynamisk uppslagning

Vi lägger till en strikt virtuell funktion void visit_tree_node(Visitor &visitor) i basklasen TreeNode. Sen implementerar vi vår funktion i alla nedärvda klasser i trädet med samma kod (se Kodexempel 2). Tanken är att detta är ett engångsjobb och att visit_tree_node inte ska behöva ändras igen.

void visit_tree_node(Visitor &visitor)
{
    visitor.do_visit(*
this);
}
Kodexempel 2: Vi lägger till samma kod i alla klasser!

Det finns förstås en bra förklaring till varför vi har en kopia av visit_tree_node i varje klass. Det är här den statiska typen på argumenten kommer in i bilden. Vi ser att do_visit tar *this som argument. Säg t.ex. att visit_tree_node anropas i klassen Variable. Då har *this den statiska typen Variable &.

Vi skapar nu en funktion do_visit(const Variable &) i vår klass Visitor. Vi anropar sen do_visit med Variable-objektet som argument. Och här kommer det fina. Vi har nu fått över objektet till Visitor-klassen med rätt typ (Variable i vårt exempel)! Därmed kan vi skriva ut objektet precis som vi vill.

Vi skapar en funktion do_visit för alla andra typer i arvshierarkin som behöver specialiserad utskrift. De typer som inte behöver specialiserad utskrift kan vi utelämna och istället skapa en do_visit(const TreeNode &) som har en defaultutskrift!

class Visitor
{
   
// skriv ut variabel
    void do_visit(
const Variable &);

   
// skriv ut alla andra typer
   
void do_visit(const TreeNode &);
};
Kodexempel 3: Vi lägger till en do_visit för alla klasser som behöver specialiserad utskrift.

Interfaceklass ger oss flera besökare

Vill vi lägga till ytterligare en typ av analys, såsom syntaxkontroll, kan vi göra om Visitor till en interfaceklass genom att göra alla funktioner strikt virtuella. Vi skapar sedan nedärvda klasser PrinterVisitor (för våra utskrifter) och SyntaxCheckVisitor som gör jobbet. Även här kommer dynamisk uppslagning in i bilden.

Visitor är ett av de mer komplexa designmönstren och man måste ju erkänna att det är smart uttänkt! Det är dessutom väldigt nyttigt i vissa speciella situationer.

Lycka till med designmönstren!



Relaterade artiklar:

 
© Johnny Bigert Data | De la Gardies gränd 22, 135 63 Tyresö | 076-782 74 00
johnny@johnnybigert.se | www.johnnybigert.se