Dynamic Instantiation in C++ (The Prototype Pattern)
Dynamic Instantiation in C++ (The Prototype Pattern)
Unfortunately,C++doesn'thavebuiltinsupportfordynamicinstantiation,buttheprototypepatternprovidesa
standardwaytoaddthisfeaturetoC++programs:
Prototype[Go4]
Problem
A"factory"classcan'tanticipatethetypeof"product"objectsitmustcreate.
Solution
DeriveallproductclassesfromanabstractProductbaseclassthatdeclaresapurevirtualclone()method.TheProductbase
classalsofunctionsastheproductfactorybyprovidingastaticfactorymethodcalledmakeProduct().Thisfunctionusesa
typedescriptionparametertolocateaprototypeinastaticprototypetablemaintainedbytheProductbaseclass.The
prototypeclonesitself,andthecloneisreturnedtothecaller.
StaticStructure
WesawthatJavaandMFCbothrequiredsomecommonbaseclassforallclassesthatcanbedynamically
instantiated:ObjectinJavaandCObjectinMFC.ThePrototypePatternalsorequiresacommonbaseclass,
whichwecallProduct.Inadditiontoservingasthebaseclassforallproducts,theProductclassmaintainsa
statictablethatholdsassociationsbetweennamesofProductderivedclasses("Product1"forexample)anda
correspondingprototypicalinstanceoftheclass.Italsoprovidesastaticfunctionforaddingnewassociationsto
theprototypetable(addPrototype),andastaticfactorymethodforcreatingproducts(makeProduct).Finally,
Productisanabstractclassbecauseitcontainsapurevirtualclone()method.Thefollowingdiagramshowsthe
ProductclasswiththreeProductderivedclasses.
TestProgram
Beforedescribingtheimplementation,let'slookatatestprogram.Ourtestdriver,main(),beginsbydisplaying
theprototypetable.Itthenentersaperpetualloopthatpromptstheuserforthenameofaproducttype,usesa
staticfactorymethodtoinstantiatethetypeentered,thenusestheRTTIfeatureofC++todisplaythetypeof
productcreated:
intmain()
intmain()
{
stringdescription
Product*p=0
cout<<"PrototypeTable:\n"
cout<<Product::protoTable<<endl
while(true)
{
cout<<"Enterthetypeofproducttocreate:"
cin>>description
p=Product::makeProduct(description)
cout<<"Typeofproductcreated="
cout<<typeid(*p).name()<<endl
deletep
}
return0
}
ProgramOutput
Curiously,thetestprogramproducesoutputbeforemain()iscalled:
addingprototypeforProduct1
done
addingprototypeforProduct2
done
addingprototypeforProduct3
done
Apparently,prototypesofthreeproductclasseshavebeenaddedtotheprototypetable.Thisisconfirmedwhen
theprototypetableisdisplayedatthebeginningofmain().Itshowspairsconsistingoftheproductnameandthe
addressofthecorrespondingprototype:
PrototypeTable:
{
(Product1,0xba1cc)
(Product2,0xba1dc)
(Product3,0xba1ec)
}
Nowtheloopbegins.Theusercreatesinstancesofeachoftheproductclasses,asconfirmedbyruntimetype
identification.(WeareusingtheDJGPPcompilerinthisexample.)Whentheuserattemptstocreateaninstance
ofanunknownclass,anerrormessageisdisplayedandtheprogramterminates:
Enterthetypeofproducttocreate:Product1
Typeofproductcreated=8Product1
Enterthetypeofproducttocreate:Product2
Typeofproductcreated=8Product2
Enterthetypeofproducttocreate:Product3
Typeofproductcreated=8Product3
Enterthetypeofproducttocreate:Product4
Error,prototypenotfound!
TheProductBaseClass
ThemainjoboftheProductbaseclassistomaintaintheprototypetable.Sincetheprototypetablecontains
associationsbetweentypenames(string)andtheaddressesofcorrespondingprototypes(Product*),wedeclareit
asastaticmapoftype:
map<string,Product*>
TheProductbaseclassalsoprovidesastaticfactorymethodfordynamicallycreatingproducts(makeProduct)
TheProductbaseclassalsoprovidesastaticfactorymethodfordynamicallycreatingproducts(makeProduct)
andastaticmethodforaddingprototypestotheprototypetable(addPrototype).Lastly,thePrototypePattern
requiresthateachproductknowshowtocloneitself.Thiscanbeenforcedbyplacingapurevirtualclonemethod
intheProductbaseclass(thisideaisrelatedtotheVirtualBodyPatterninChapter4).Here'sthedeclaration:
classProduct
{
public:
virtual~Product(){}
virtualProduct*clone()const=0
staticProduct*makeProduct(stringtype)
staticProduct*addPrototype(stringtype,Product*p)
staticmap<string,Product*>protoTable
}
Recallthatthedeclarationofastaticclassvariable,likeprotoTable,isapuredeclarationthatsimplybindsa
name(protoTable)toatype(map<>).Novariableisactuallycreated.Instead,thismustbedonewithaseparate
variabledefinition.AssumingtheProductclassisdeclaredinafilecalledproduct.h,wemightwanttoplacethe
definitionoftheprototypevariableatthetopofthefileproduct.cpp:
//product.cpp
#include"product.h"
map<string,Product*>Product::protoTable
Ourproduct.cppimplementationfilealsocontainsthedefinitionsofthemakeProduct()factorymethodandthe
addPrototype()function.
ThemakeProduct()factorymethodusestheglobalfind()functiondefinedinutil.h(whichislistedinAppendix3
andshouldbeincludedatthetopofproduct.h)tosearchtheprototypetable.Theerror()functiondefinedinutil.h
isusedtohandletheerrorifthesearchfails.Otherwise,theprototypelocatedbythesearchiscloned,andthe
cloneisreturnedtothecaller:
Product*Product::makeProduct(stringtype)
{
Product*proto
if(!find(type,proto,protoTable))
error("prototypenotfound")
returnproto>clone()
}
TheaddPrototype()functionhastwoparametersrepresentingthenameofaProductderivedclass("Product1"
forexample)andapointertoaprototypicalinstanceofthatclass.Thefunctionsimplyaddsanewassociationto
theprototypetable.Fordebuggingpurposes,thestatementissandwichedbetweendiagnosticmessages.Iffor
somereasonwefailtoaddaparticularprototypetotheprototypetable,wewillknowexactlywhichonecaused
problems.(Moreonthislater.)Finally,noticethattheprototypepointerisreturned.Thepurposeofthisreturn
statementwillalsobeexplainedlater.
Product*Product::addPrototype(stringtype,Product*p)
{
cout<<"addingprototypefor"<<type<<endl
protoTable[type]=p
cout<<"done\n"
returnp//handy
}
CreatingProductDerivedClasses
CreatingProductDerivedClasses
Onemeasureofqualityforaframeworkishoweasyitistocustomize.Frameworkswithheavyoverhead(i.e.,
thatrequirecustomizerstowritehundredsoflinesofcodebeyondwhattheyalreadyhavetowrite)areoften
veryunpopular.HowmuchextraworkisittoderiveaclassfromourProductbaseclass?Onlyfourextralinesof
codearerequired.
AssumewewanttodeclareaclassnamedProduct1inafilenamedproduct1.h.Wewanttobeableto
dynamicallyinstantiateProduct1,sowemustderiveitfromtheProductbaseclass.Theboldfacelinesshowthe
overheadimposedbytheProductbaseclass:
//product1.h
#include"product.h"
classProduct1:publicProduct
{
public:
IMPLEMENT_CLONE(Product1)
//etc.
}
AssumetheProduct1classisimplementedinafilenamedproduct1.cpp.Wemustaddasinglelinetothatfile,
too:
//product1.cpp
#include"product1.h"
MAKE_PROTOTYPE(Product1)
//etc.
Macros
IMPLEMENT_CLONE()andMAKE_PROTOTYPE()aremacrosdefinedinproduct.h.Recallthatmacrocalls
areexpandedbytheCpreprocessorbeforecompilationbegins.Forexample,ifaprogrammerdefinesthe
followingmacro:
#definePI3.1416
AllcallsoroccurrencesofPIinaprogramarereplacedbythevalue3.1416.
Macroscanalsohaveparameters.Inthiscaseanargumentisspecifiedwhenthemacroiscalled,andthe
expansionprocessautomaticallysubstitutestheargumentforalloccurrencesofthecorrespondingparameterin
themacro'sbody.
Forexample,eachProductderivedclassmustimplementthepurevirtualclone()functionspecifiedinthe
Productbaseclass.Infact,theimplementationsaresimpleandwon'tvarymuchfromoneclasstothenext.
Thereisarisk,however,thatprogrammersmightgettoocreativeandcomeupwithanimplementationthat'stoo
complexorjustplainwrong.
Toreducethisrisk,weprovidetheIMPLEMENT_CLONE()macro,whichisparameterizedbythetypeof
producttoclone.Themacrobodyistheinlineimplementationoftherequiredclonefunction:
#defineIMPLEMENT_CLONE(TYPE)\
Product*clone()const{returnnewTYPE(*this)}
(Noticethatmacrodefinitionsthespanmultiplelinesuseabackslashcharacterasalineterminator.)
WeplacedacalltothismacrointhedeclarationoftheProduct1class:
classProduct1:publicProduct
{
public:
IMPLEMENT_CLONE(Product1)
//etc.
}
AfterpreprocessorexpandsthiscallthedeclarationofProduct1willlooklikethis:
classProduct1:publicProduct
{
public:
Product*clone()const{returnnewProduct1(*this)}
//etc.
}
NoticethattheTYPEparameterinthemacrobodyhasbeenreplacedbytheProduct1argument,formingacall
totheProduct1copyconstructor.Readersshouldverifythattheimplementationcorrectlyreturnsacloneofthe
implicitparameter.
CreatingPrototypes
Theoutputproducedbyourtestprogramshowedthreeprototypeswerecreatedandaddedtotheprototypetable
beforemain()wascalled.Howwasthatdone?Ingeneral,howcanprogrammersarrangetohavecodeexecuted
beforemain()?Isn'tmain()thefirstfunctioncalledwhenaC++programstarts?
Actually,wecanarrangetohaveanyfunctioncalledbeforemain(),providedthatfunctionhasareturnvalue.For
example,thefunction:
inthello()
{
cout<<"Hello,World\n"
return0//abogusreturnvalue
}
willbecalledbeforemain()ifweuseitsreturnvaluetoinitializeaglobalvariable:
intx=hello()//x=abogusglobal
Thiscanbeverifiedbyplacingadiagnosticmessageatthebeginningofmain()andobservingthatthe"Hello,
World"messageappearsfirst.
RecallthattheaddPrototype()functionreturnedapointertotheprototype.Ifweusethisreturnvaluetoinitialize
abogusglobalProductpointervariable:
Product*Product1_myPrototype=
Product::addPrototype("Product1",newProduct1())
thenthecalltoaddPrototype()willprecedethecalltomain().Inprinciple,wecanbuildtheentireprototype
tablebeforemain()iscalled.
OurMAKE_PROTOTYPE()macroexpandsintodefinitionsliketheoneabove:
#defineMAKE_PROTOTYPE(TYPE)\
Product*TYPE##_myProtoype=\
Product::addPrototype(#TYPE,newTYPE())
Duringexpansion,themacroparameter,TYPE,willbereplacedbythemacroargument,Product1forexample,
inthreeplaces.First,the##operatorisusedtoconcatenatethetypenamewith_myPrototype.Inourexample
inthreeplaces.First,the##operatorisusedtoconcatenatethetypenamewith_myPrototype.Inourexample
thisproducesProduct1_myPrototype,a(hopefully)uniquenameforaglobalvariable.
Second,the#operatorisusedtostringifytheargument.IftheargumentisProduct1,#TYPEwillbereplacedby
"Product1",thestringnameofthetype.
Finally,thelastoccurrenceoftheTYPEparameterwillbereplacedbyacalltothedefaultconstructorspecified
bytheargument.
LinkerIssues
Addingentriestotheprototypetablebeforemain()iscalledisrisky.Recallthatthedefinitionoftheprototype
tableoccursinproduct.cpp,whilethecallstotheMAKE_PROTOTYPE()macrooccurinthefilesproduct1.cpp,
product2.cpp,andproduct3.cpp.Thesefileswillcompileintotheobjectfilesproduct.o,product1.o,product2.o,
andproduct3.o,respectively,whichwillbelinkedwithmain.obythelinker(ld).Theactuallinkcommandmight
looklikethis:
lddemomain.oproduct.oproduct1.oproduct2.oproduct3.o
Supposetheorderofobjectfileargumentspassedtothelinkerismodified:
lddemomain.oproduct1.oproduct.oproduct2.oproduct3.o
Intheexecutableimageproducedbythelinker,thedeclaration:
Product*Product1_myPrototype=
Product::addPrototype("Product1",newProduct1())
containedinproduct1.omayprecedethecreationoftheprototypetablecontainedinproduct.o:
map<string,Product*>Product::protoTable
SincethecalltoaddPrototype()attemptstoinstallapairintotheprototypetable,thiswillresultinamysterious
programcrashthatdefinesmostdebuggers,becausetheproblemoccursbeforemain()iscalled.(Inourcasethis
bugwillbeeasytocatchthankstothediagnosticmessagesprintedbyaddPrototype.)
Theproblemiseasilyrectifiedifweabandontheideaofaddingentriestotheprototypetablebeforemain()is
called.Inthiscasethemacrocallswouldbemadeatthetopofmain():
intmain()
{
MAKE_PROTOTYPE(Product1)
MAKE_PROTOTYPE(Product2)
MAKE_PROTOTYPE(Product3)
//etc.
}