Comments 0

Document transcript

Creating aC++ScriptingSystemS C R I P T I N Gj u n e 2 0 0 2|g a m e d e v e l o p e r40e m i l d o t c h e v s k iMost of today’s games require some kind ofsystem to allow the game designers to pro-gram the story of the game, as well as anyfunctionality that is not general enough to bedirectly supported by the AI or some othersystem. Often, the scripting system of a game is also madeavailable to players for customization. Today it is not uncom-mon for third-party companies to customize an existing gameto incorporate completely different gameplay, in part by chang-ing the game scripts.Depending on the game type, the best results are achievedusing different approaches to scripting. In environments thathave very well defined rules, such as RTS games, the most impor-tant task of the designer is to achieve good balance between thedifferent units and resources, while also producing interestingmaps with features that clever players can use to their advantage.Clearly, the scripting support for such games is focused on fastand easy tweaking of the different parameters exposed by thegame engine, and is usually directly supported by the map editor.Other games, for example many FPS titles, require very limit-ed levels of customization. This is usually done by taggingobjects in the game editor.And finally, many games are driven by complex enough logicto require a complete programming language for scripting. Insome cases, developers have designed and implemented theirown programming languages to serve this need. To cut develop-ment time, today most game companies opt to use existing pro-gramming languages customized for their own scripting needs.Scripting LanguagesMost so-called scripting languages have one thing in com-mon: they have been developed by a small team or evena single person for the purpose of writing simple programs tosolve a particular, limited set of problems. By their very naturethey are not universal, yet many end up being employedbeyond their intended purpose.Sometimes, “scripting language” simply means a program-ming language used to write scripts. Indeed, people have suc-cessfully used languages such as Java for scripting.When selecting a scripting language for a game, chances arethat a developer will not find a language that matches thedesired functionality completely. At the very least, a developerwill have to design and implement an interface between thegame engine and the scripting language. The programminglanguage the developer chooses is just one of the components— often not the most important one — of a scripting engine.Using C++ for ScriptingOne of the most important characteristics of C++ is itsdiversity. Unlike many other languages that are efficientfor a particular programming style, C++ directly supports sever-al different programming techniques. It’s like a bag of tricksthat allows many, often very different solutions to a problem.Some of the C++ features — such as function and class tem-plates — are so powerful that even the people who developedand standardized them could not have foreseen the full spec-trum of problems they can solve.The idea of using C++ for scripting may seem strange at first.Indeed, most people associate C++ with pointers and dynamicmemory management, which are powerful features but aremore complex to work with than what most game designerswould consider friendly.On the other hand, C++ is the natural choice to program therest of the game in. If the scripts are also written in C++, thenintegration with the rest of the code is seamless. In addition,C++ is translated to highly optimized machine code. While speedis rarely a problem for most game scripts, faster is always better.Naturally, using C++ for scripting has some drawbacks.Because it is a compiled (as opposed to interpreted) language, aC++ program can’t be changed on the fly, which is important insome applications. Also, C++ does not provide a standard forplugging in program modules at run time, which makes it near-ly impossible to expose the script for customization by users(usually this is not an issue for console titles).It’s important to create a safe and easy-to-use environmentfor our scripts. The language used is a secondary concern,because by their very nature scripts are simple and mostly lin-early executed, with limited use of if-then-else or switch state-EMI L DOT CHEVSKI | Emil is currently co-lead programmer at Tremor Entertainment on a soon-to-be-announced original Xboxtitle. His previous work includes RAILROADTYCOONII for Playstation and an enhanced 3D version for Dreamcast. Emil has an M.S. incomputer science from the Sofia University, Bulgaria. He can be reached at emil@tremor.net.w w w.g d m a g.c o mments. In this article we will demonstrate how to use someadvanced C++ features to create a safe “sandbox” environmentfor the scripts within our game code.Creating a Safe ScriptingEnvironmentMore often than not, programmers design a class hierarchyto organize the objects that exist in the game’s digitalreality. For the purposes of this article, let us assume that ourgame uses the classes whose partial declaration is given inListing 1.Using C++ for script-ing allows us to useplain pointers for inter-facing with the scriptcode. The main draw-back of using pointersis that they can pointto invalid memory. Thebenefits are that point-ers are directly sup-ported by the languageand are very efficient.Also, pointers make iteasy for programmersto implement and laterextend the interfacebetween the scripts andthe game.To eliminate the pos-sibility for the scriptcode to access invalidmemory, the use ofpointers must be hidden from the scripts. In C++, we can dothis by organizing the pointers in a set container.Then we candefine functions and operations for working with entire sets of(pointers to) objects. This neatly unifies the processing of oneor more objects and usually allows the scripts to not have tohandle empty sets as a special case.For example, to represent a set of CGrunt objects (see Listing1), we can use something like this:std::set<CGrunt*> grunts;Let’s also define a functor to expose the CActor::Attack func-tion to the script:struct Attack {void operator()( CActor* pObj ) const {pObj->Attack();}};Now we can use std::for_each with the function objectAttack to have all the grunts in the set use their attack func-tionality:std::for_each( grunts.begin(), grunts.end(), Attack() );The for_each template function is defined so that it can workwith any sequence of objects, which is a level of flexibility wedo not need. Instead, we can define our own version of for_each,which for convenience we can simply call X (from Execute):template <class Set,class Functor>void X( const Set& set, Functor f ) {for( typename Set::const_iterator i=set.begin(); i!=set.end(); ++i )f(*i);}Now we can simply say:X( grunts, Attack() );It’s also possible to write functors that take arguments andpass them to the function they call:41class CRoot {virtual ~CRoot();};class CActor: public CRoot {public:bool HasWeapon() const;bool HasArmor() const;int GetHealth() const;void SetHealth( int health );void Attack();...};class CGrunt: public CActor {public:...};class CAgent: public CActor {public:...};LISTING 1.An example class hierarchy.S C R I P T I N Gstruct SetHealth {int health;SetHealth( int h ): health(h) {}void operator()( CActor* pObj ) const {pObj->SetHealth(health);}};Now we can use the SetHealth functor like so:X( grunts, SetHealth(5) );PredicatesUsing function objects to perform actions on an entire set ofobjects is a powerful feature by itself, but it becomes evenmore powerful if we define another version of the X functionthat allows us to call the function object only for selectedobjects in a set:template <class Set,class Pred,class Functor>void X( const Set& set, Pred p, Functor f ) {for( typename Set::const_iterator i=set.begin(); i!=set.end(); ++i )if( p(*i) )f(*i);}A predicate is a special type of function object that checks agiven condition. For example, we can define the followingpredicate:struct HasArmor {bool operator()( const CActor* pObj ) const {return pObj->HasArmor();}};Now we can write something like:X( grunts, HasArmor(), Attack() );This will execute the Attack functor on the members of thegrunts set that are armored. Again, exposing the armor propertyof objects of class CActor to the script is as easy as writing a sim-ple predicate.Type PredicatesIn the preceding examples, because Attack::operator() takesCActor* as an argument, the Attack functor can only be usedwith sets of objects of class CActor. Because any object of classCGrunt is also of class CActor, we can use the Attack functor witha set of grunts too. But what if we have a set of objects of classCRoot? It would be nice to be able to select only the objects ofclass CActor and execute Attack on them.To do this, we need our predicates to define an output_type.Then we can design our system so that if an object passes apredicate, we can assume it is of class output_type that the predi-cate defines. For example, we could use the predicate IsActorthat checks if a given object is of class CActor:struct IsActor {typedef CActor output_type;bool operator()( const CRoot* pObj ) const {return 0!=dynamic_cast<const CActor*>(pObj);}};Note in this case that some compilers do not implement dynam-ic_cast efficiently because it has to work in nontrivial cases suchas multiple inheritance and the like. Instead of dynamic_cast, wecould use a virtual member function to do our type checks.We also need to modify the predicate version of our X tem-plate function:template <class Set,class Pred,class Functor>void X( const Set& set, Pred p, Functor f ) {for( typename Set::const_iterator i=set.begin(); i!=set.end(); ++i )if( p(*i) )f( static_cast<typename Pred::output_type*>(*i) );}The output_type defined by our predicates makes it safe for theX template function to use a static_cast when calling the functor.Now, if we have a set of objects of class CRoot, we can write:X( objects, IsActor(), Attack() );Complex PredicatesSo now we have seen how to use predicates to execute a functoron selected objects from a given set of objects. But what if wewant to combine multiple predicates to select the objects we need?Generally speaking, it’s easy to combine multiple simplepredicates in a single complex predicate that our X function cancheck. As an example, let’s define a predicate called pred_or:template <class Pred1,class Pred2>struct pred_or {Pred1 pred1;Pred2 pred2;pred_or( Pred1 p1, Pred2 p2 ): pred1(p1),pred2(p2) {}bool operator()( const CActor* pObj ) const {return pred1(pObj) || pred2(pObj);}};To make it possible to use pred_or without having to explicit-ly provide template arguments, we can define the followinghelper function template:template <class Pred1,class Pred2>pred_or<Pred1,Pred2> Or( Pred1 p1, Pred2 p2 ) {return pred_or<Pred1,Pred2>(p1,p2);}j u n e 2 0 0 2|g a m e d e v e l o p e r42With the Or function template in place, we can use pred_or tocombine the HasArmor and the HasWeapon predicates:X( grunts, Or(HasArmor(),HasWeapon()), Attack() );But wait, why did we define pred_or::operator() to takeCActor*? This is not ideal, because we want pred_or to be able tocombine predicates that take objects of different classes. Inaddition, our X function requires us to define an output_type.What is the output_type for pred_or?Let’s extend our system to require that the predicates definenot only output_type but also input_type, which is the class ofobjects the predicate can be checked for. With this in mind, let’sdefine pred_or as shown in Listing 2.For this to work, we need two helper template classes,select_child and select_root. We see how they work later, but fornow let’s just assume the following: select_root<T,U>::type isdefined as the first class in the class hierarchy that is commonparent of both T and U, or as void if T and U are unrelated. Forexample, select_root<CGrunt,CAgent>::type will be defined as CActor.select_child<T,U>::type is defined as T if T is a (indirect) childclass of U, as U if U is a (indirect) child class of T, or as void other-wise. For example, select_child<CRoot,CGrunt>::type is defined asCGrunt, while select_child<CGrunt,CAgent>::type is defined as void.Indeed, for pred_or::operator() to return true, either the firstor the second predicate should have returned true. Since wedo not know which predicate returned true, our output_type isthe root class of the output_types defined by the Pred1 and Pred2predicates.Similarly, because Pred1::operator() takes objects of classPred1::input_type, and Pred2::operator() takes objects of classPred2::input_type, pred_or::operator() must take objects that areof both class Pred1::input_type and class Pred2::input_type. This iswhy we need select_child.Now let’s define pred_and as shown in Listing 3. Here, thestatic_cast is justified because C++ always evaluates the left sideof operator&& first, and then evaluates the right side only if theleft side was true — and we know that if an object passes Pred1,it is of class Pred1::output_type.Let’s extend the definition of HasArmor to define input_type andoutput_type as required:struct HasArmor {typedef CActor input_type;typedef input_type output_type;bool operator()( const input_type* pObj ) const {return pObj->HasArmor();}};Now, if we have a set of objects of class CRoot, we can dosomething like this (assuming we have defined a function tem-plate And similar to the function template Or):X( objects, And(IsActor(),HasArmor()), Attack() );We can even combine pred_and and pred_or in an even morecomplex predicate expression:X( objects, And(IsActor(),Or(HasArmor(),HasWeapon())),Attack() );To complete our set of complex predicates, let’s definepred_not. Because not satisfying a predicate does not give us anyadditional information about an object, pred_not::output_type isthe same as its input_type:template <class Pred>struct pred_not {typedef typename Pred::input_type input_type;typedef input_type output_type;Pred pred;pred_not( Pred p ): pred(p) {}bool operator()( const input_type* pObj ) const {return !pred(pObj);}};Besides being powerful, the complex predicates we definedare also type-safe. Consider the following example:X( objects, Or(IsActor(),HasArmor()), Attack() );j u n e 2 0 0 2|g a m e d e v e l o p e r44LISTING 2.Defining pred_or.LISTING 3.Defining pred_and.template <class Pred1,class Pred2>struct pred_and {typedef typename Pred1::input_typeinput_type;typedef typename select_child<typename Pred1::output_type,typename Pred2::output_type>::typeoutput_type;Pred1 pred1;Pred2 pred2;pred_and( Pred1 p1, Pred2 p2 ): pred1(p1),pred2(p2) {}bool operator()( const input_type* pObj ) const {return pred1(pObj) &&pred2(static_cast<const typenamePred1::output_type*>(pObj));}};template <class Pred1,class Pred2>struct pred_or {typedef typename select_child<typename Pred1::input_type,typename Pred2::input_type>::typeinput_type;typedef typename select_root<typename Pred1::output_type,typename Pred2::output_type>::typeoutput_type;Pred1 pred1;Pred2 pred2;pred_or( Pred1 p1, Pred2 p2 ): pred1(p1),pred2(p2) {}bool operator()( const input_type* pObj ) const {return pred1(pObj) || pred2(pObj);}};S C R I P T I N GIf used with our class hierarchy, the above example will notcompile. This is because if an object passes the predicate, it mayor may not be of class CActor, and the compiler will generate atype mismatch error when trying to call HasArmor::operator().However, if we used And instead of Or, there would be no com-pile error due to the static_cast in pred_and::operator().Predicate ExpressionsSo far, our predicate system is pretty powerful, but nestedpredicate expressions are not fun. We need to be able to usea more natural syntax. For example, instead ofX( objects, And(IsActor(),HasArmor()), Attack() );we want to be able to write:X( objects, IsActor() && HasArmor(), Attack() );The obvious solution to this problem is to overload the opera-tors we need for predicates. For the system to work, we need tooverload the operators in a way that can be used for any predi-cates, including custom predicates defined further in our project.However, if we define operator&& the way we earlier definedthe Or function template, it would be too ambiguous — weneed a definition that the compiler will consider for predicatesonly. To achieve this, we need some mechanism to distinguishbetween a predicate and any other type. One way of doingthis is to have all our predicates inherit from this commonclass template:template <class T>struct expr_base {const T& get() const {return static_cast<const T&>(*this);}};For example, let’s define pred_and like this:template <class Pred1,class Pred2>struct pred_and: public expr_base<pred_and<Pred1,Pred2> >{...};With this trick, we can overload operator&& like so:template <class Pred1,class Pred2>pred_and<Pred1,Pred2>operator&&( const expr_base<Pred1>& p1, const expr_base<Pred2>& p2 ) {return pred_and<Pred1,Pred2>(p1.get(),p2.get());}Following this pattern, we can overload operator || andoperator !. Now we can build Boolean predicate expressionsthat follow the natural C++ syntax, while also taking advantageof the operator precedence defined by the language.This technique of building an expression tree through opera-tor overloads is commonly known as Expression Templates (seeVeldhuizen in For More Information).Numerical PredicatesPredicates are usually defined as Boolean functions, but wecan extend our definition of predicate to include numericalpredicates. This is useful for exposing non-Boolean properties ofobjects. For example:struct Health {typedef CActor input_type;typedef input_type output_type;int operator()( const input_type* pObj ) const {return pObj->GetHealth();}};Of course, the predicate version of the X template functiontreats all predicates as Boolean. We can use the Health predicatedirectly, but then we would only be able to check if the health ofan actor is not 0 (or we can check for 0 if we use pred_not).Obviously, we need to be able to check for other values as well.So, we can define the following predicate:template <class Pred,class Value>struct pred_gt: public expr_base<pred_gt<Pred,Value> > {typedef typename Pred::input_type input_type;typedef typename Pred::output_type output_type;Pred pred;Value value;pred_gt( Pred p, Value v ): pred(p),value(v) {}bool operator()( const Pred::input_type* pObj ) const {return pred(pObj)>value;}};Similarly to the case of pred_or, pred_and, and pred_not, we canoverload the > operator to provide access to pred_gt:template <class Pred,class Value>pred_gt<Pred,Value>operator>( const expr_base<Pred>& p, Value v ) {return pred_gt<Pred,Value>(p.get(),v);}Now, we can do something like:X( grunts, Health()>5, Attack() );This will execute the Attack functor only on the objects withhealth greater than 5. As any other predicate that definesinput_type and output_type, we can combine pred_gt in complexpredicate expressions. For example:X( objects, IsGrunt() && (Health()>5 || HasArmor()), Attack() );Following this pattern, we can overload all other comparisonoperators: <,>=,<=,==,and!=.j u n e 2 0 0 2|g a m e d e v e l o p e r46S C R I P T I N GAdditional SetOperationsThe predicate expression system we justdescribed is the core of our scripting sup-port, but we still need to write some addition-al functions to make it easy to work with sets.Table 1 shows some additional function tem-plates that we may find useful to define.In addition, it is convenient to define opera-tor functions for set intersection (*,*=), set union(+,+=), and set difference (-,-=). All of these func-tions can be defined as templates for maximumflexibility.Integration with theGameWe have a powerful system to manipu-late sets of objects, but to be able todo anything with it, we need to define theinterface between the script and the game.For example, it could be appropriate todefine a class that is the root of all scripts:class CScriptBase {public:void RegisterObject( CRoot* pObject );void RemoveObject( CRoot* pObject );virtual void Tick( float deltaTime )=0;...protected:set<CRoot*> m_Objects;set<CRoot*> CheckArea( const char* areaName );...};The public section of our class contains functions that thegame can execute. RegisterObject is called from the constructorof CRoot whenever a game object is created. The task ofRegisterObject is to filter out any objects we do not want thescript to have access to, and include all other objects in them_Objects set which is accessible by the script. Similarly,RemoveObject is called from the destructor of CRoot to make surem_Objects does not contain pointers to invalid objects. The gamealso calls Tick on each frame to let the script do its job. We cancontinue along these lines, but obviously there is not much thegame has to know about the script.The protected section of our class has functions that the childscript classes can use to query the game for information. Allsuch functions are implemented by CScriptBase. In our example,CheckArea will check a named area in the level for any objectsand return a set that contains them. Once the script has the set,it can use the predicate system to get the information it needs.For example, to check if the player has advanced to a givenarea, we can do something like this:if( Any(CheckArea(“Area1”),IsPlayer()) )SignalPlayerIsInArea1();Besides query functions, it is also useful to define functionsthat make it easier for the script to perform common tasks.This could include, for example, the ability to register a mem-ber function of the script for automatic periodic execution.Defining Additional TemplatesTo operate properly, our predicate system depends on theselect_root and select_child templates. The C++ languagedoes not provide direct support for something like that, but wecan trick the compiler into doing what we need with some metaprogramming.We will need to associate a numerical identifier with eachclass of our class hierarchy. To do this, we can define the fol-lowing templates:template <class T>struct get_id {enum {value=0};};template <int ClassID>struct get_type {typedef void type;};To associate identifiers with classes, we simply define explicitspecializations of get_id and get_type. For example:w w w.g d m a g.c o m47TABLE 1.Some additional functions that enhance usability of sets.All(set,predicate)Num(set)Num(set,predicate)Any(set)Any(set,predicate)FirstFew(set,count)FirstFew(set,predicate,count)Some(set,count)Some(set,predicate,count)Returns a set that contains all the elements of the inputset that satisfy the predicateReturns the number of elements in the setReturns the number of the elements in the set thatsatisfy the predicateReturns true if the set contains at least one element,false otherwiseReturns true if the set contains at least one element thatsatisfies the predicateReturns a set that contains the first count elements ofthe input setReturns a set that contains the first count elements ofthe input set that satisfy the predicateReturns a set that consists of count random elementsfrom the input setReturns a set that consists of count random elementsfrom the input set that satisfy the predicatetemplate<>struct get_id<CGrunt> {enum {value=30};};template<>struct get_type<30> {typedef CGrunt type;};Now, get_id<CGrunt>::value evaluates to 30, andget_type<30>::type evaluates to CGrunt.In addition, let’s define the following template:template <class T>struct tag {typedef char (&type)[get_id<T>::value];};For a given class T, this template defines a reference to a chararray the size of the numerical identifier associated with T.Finally, to continue our CGrunt example, let’s declare the fol-lowing function:tag<CGrunt>::type caster( const CGrunt*,const CGrunt*);Note that we only declare this function. We do not provide adefinition.For select_root to work, all of the classes in our hierarchymust be properly registered by providing explicit specializationof get_id and get_type, plus a declaration of the caster function.This is best done using a macro:#define REGISTER_CLASS(CLASS,CLASSID)\template<> struct get_id<CLASS> { enum {value=CLASSID}; };\template<> struct get_type<CLASSID> { typedef CLASS type; };\tag<CLASS>::type caster( const CLASS*,const CLASS*);Now let’s register CRoot,CActor,CGrunt, and CAgent by invokingthe REGISTER_CLASS macro, using a different numerical identifierfor each class:REGISTER_CLASS(CRoot,10)REGISTER_CLASS(CActor,20)REGISTER_CLASS(CGrunt,30)REGISTER_CLASS(CAgent,40)With the classes properly registered, the select_root templatecan be defined like this:template <class T,class U>struct select_root {enum { ClassID=sizeof(caster((T*)0,(U*)0)) };typedef typename get_type<ClassID>::type type;};Now let’s follow what happens when we use select_root withCGrunt and CAgent (this is all done at compile time):typename select_root<CGrunt,CAgent>::type* pObj;We have invoked the REGISTER_CLASS macro for CRoot, CActor,CGrunt, and CAgent. As a result, now we have the following func-tion declarations:tag<CRoot>::type caster( const CRoot*,const CRoot*);tag<CActor>::type caster( const CActor*,const CActor*);tag<CGrunt>::type caster( const CGrunt*,const CGrunt*);tag<CAgent>::type caster( const CAgent*,const CAgent*);In our select_root template, we define ClassID as the size ofthe type returned by caster((T*)0,(U*)0). In our example, T isCGrunt and U is CAgent. Since we have not declared a version ofthe caster function that takes CGrunt* and CAgent*, the compilerautomatically picks the best match from the ones we diddeclare, which is:tag<CActor>::type caster( const CActor*,const CActor*);This defines ClassID as sizeof(tag<CActor>::type). If you recallhow the tag template was defined, tag<CActor>::type is a refer-ence to a char array of size get_id<CActor>::value.Sinceget_id<CActor>::value is 20, select_root<CGrunt,CAgent>::ClassIDwill also be 20. Now we simply use get_type<ClassID>::type toretrieve the class the number 20 identifies, which is CActor.So, if we return back to our example,typename select_root<CGrunt,CAgent>::type* pObj;pObj will be defined as pointer to object of class CActor.And finally, Listing 4 shows how we can define select_child.Here, meta_if is a template that’s commonly used for meta pro-gramming. I’ll skip its definition, but assume that meta_if<CONDI-TION,T,U>::type is defined as T if the condition is nonzero, and asU otherwise.The implementation of select_root and select_child could besimplified if C++ had compile-time typeof() fuctionality. Theemulation of typeof() through type registration used here wasfirst discovered by Bill Gibbons (see For More Information).Safety and PerformanceConsiderationsOne of the most important features of our scripting systemis that it is type-safe. Thanks to the input_type andoutput_type each predicate defines, the compiler knows the classof the objects that pass a given predicate expression and willissue error messages if we try to use incompatible functors withj u n e 2 0 0 2|g a m e d e v e l o p e r48template <class T,class U>struct select_child{typedeftypename meta_if<get_id<typenameselect_root<T,U>::type>::value==get_id<T>::value, //ifU, //thentypename meta_if< //elseget_id<typenameselect_root<T,U>::type>::value==get_id<U>::value, //ifT, //thenvoid //else>::type>::type type;};LISTING 4.Defining select_child.S C R I P T I N Gthem. Predicate expressions that make no sense — for example,IsGrunt() && IsAgent() — will not compile. In addition, predicateexpressions will benefit from the compiler’s expression short-circuit logic.To further improve safety, we can avoid using pointers in oursets. In this case we can convert back to pointers just before wecall functors and predicates from our X function (or any otherhelper function that works with sets). Note that only one con-version to pointer per object occurs, regardless of how complexthe predicate expression we use is.We can further separate the script and the game code by put-ting them in their own namespaces. Thus we can control what tohide from the script, and what to exposeby providing functors and predicates.Most of today’s compilers will opti-mize any of our predicate expressions toinline code as if a programmer wrote acustom if statement to check for the con-dition of the predicate. This means thatthe performance of our scripting systemdepends mostly on the implementation ofstd::set and the iterator classes it defines.The C++ standard mandates that theiterators of std::set are not invalidatedwhen adding or removing elements fromthe set, which usually means that eachelement of the set is allocated as individ-ual heap block. Because the elements ofour sets are simply pointers, this trans-lates to a waste of memory, increasedheap fragmentation, and overall slowprocessing of std::sets due to cachemisses. In addition, copying a std::set ofpointers is a relatively slow and heap-intensive operation.Even if we do not do anything tospeed the system up, our scripts willmost likely execute faster than if weused an interpreted scripting language.Still, we can speed them up significantlyby using a custom allocator with thestd::set class template, or by designingour own, faster set container.Example Source CodeThe source code available for down-load at www.gdmag.com uses theclass hierarchy from Listing 1 to demon-strate the ideas discussed in this article,but it is a bit more complex because ithas added support for const and volatiletype modifiers. The code has been testedand is compatible with Visual C++ ver-sion 6 and 7, but should be compatiblewith most of today's C++ compilers.qw w w.g d m a g.c o m49ACKNOWLE DGE ME NTSI would like to thank Peter Dimov for his help in implementing andfurther enhancing the predicate expressions system described inthis article.F OR MORE I NF ORMATI ONT. Veldhuizen. “Expression Templates.” C++ Report Vol. 7, No. 5,(June 1995) www.osl.iu.edu/~tveldhui/papers/Expression-Templates/exprtmpl.htmlB. Gibbons. “A Portable typeof Operator”www.accu-usa.org/2000-05-Main.html