Conversion to Release 13 is easy, once you know how to change things. This document walks you through several changes anticipated for MacApp developers who are migrating code, so that you can become more familiar with the process. All of the example code is based on the conversion of real-world application code.
This document illustrates how to migrate the following constructs:
If you would like to examine the example source code all at once, you can review to the Summary sections:
TObject was removed from MacApp in favor of using more lightweight mixin classes MStreamable_AC, MDependable_AC, and RTTI macros, which significantly reduces virtual table sizes within MacApp and in your own code. For your classes that don't use or override any methods now found in MStreamable or MDependable, you no longer need a base class. If you do override or use any methods of MStremable or MDependable, you should subclass from one or both of these classes. RTTI changes are covered in a later section.
Consider the example:
class TMyClass : public TObject { public: ... virtual void ReadFrom(TStream* itsStream); ... };
TMyClass must change its base class to MStreamable because it overrides stream methods, but it neither uses nor overrides MDependable methods, so it does not need to add that class as a base class. The class TStream now belongs in ACS, and so uses the name CStream_AC. Note: If the class previously did require the use of streams, dependency, or RTTI, the class could simply be declared without a superclass.
class TMyClass : public MStreamable_AC { public: ... virtual void ReadFrom(CStream_AC* itsStream); // override };
In most places in MacApp, polymorphic initialization was occuring in places other than I methods or was not required, and this allowed MacApp and ACS to integrate I methods with constructors. This enables the compiler to better optimize constructors, and generally allows for more compact code for creating and initializing objects. In general, MacApp and ACS preserve the same arguments from the I method in the constructor, where this was possible, but in a few cases, obsolete parameters and parameters requiring polymorphic initialization were removed in deference to alternatives.
Consider the example:
class TMyCommand : public TCommand { public: TMyCommand(MyDataHandle myDataHandle); void IMyCommand(TMyClass* itsMyClass); ... MyDataHandle fMyDataHandle; }; TMyCommand::TMyCommand(MyDataHandle myDataHandle) { fMyDataHandle = myDataHandle; } TMyCommand::IMyCommand(TMyClass* itsMyClass) { TDocument* itsDocument = NULL; itsMyClass->DoSomeCommandPreInitialization(itsDocument); ICommand(cMyCommand, itsDocument, TRUE, TRUE, itsDocument); }
Here is some sample code that creates the command:
void TMyClass::DoSomethingThatMayFail() { cmd = new TMyCommand(myData); cmd->IMyCommand(this); }
Because there are no more I methods, IMyCommand will be combined with the constructor. However, TMyCommand's constructor performs some command pre-initialization before calling ICommand, and we can't insert code before the constructor calls its base constructor. TCommand constructor arguments are exactly the same as ICommand, so the same parameters will be passed in.
class TMyCommand : public TCommand { public: TMyCommand(MyDataHandle myDataHandle, TDocument* itsDocument); ... MyDataHandle fMyDataHandle; }; TMyCommand::TMyCommand(MyDataHandle myDataHandle, TDocument* itsDocument) : TCommand(cMyCommand, itsDocument, true, true, itsDocument), fMyDataHandle(myDataHandle) { }
The function instantiating the command is now responsible for calling DoSomeCommandPreInitialization before constructing the command. Note: the call to new is replaced with TH_new, a macro that allows debug builds to record file locations when memory leaks are found.
void TMyClass::DoSomethingThatMayFail() { TDocument* itsDocument = NULL; this->DoSomeCommandPreInitialization(itsDocument); cmd = TH_new TMyCommand(myData, itsDocument); }
Isn't freeing member data a chore? Don't you wish you could just say "delete this object when the object of which it is a member gets deleted"? By using auto-pointers such as ACS's CAutoPtr_AC, you can. ACS also has a handy templatized handle class which behaves similar to an auto-pointer.
Consider this example using the command object's member data from the previous section:
class TMyCommand : public TCommand { public: virtual Free(); ... MyDataHandle fMyDataHandle; }; void TMyCommand::Free() { fMyDatahandle = (MyDataHandle) FreeIfObject((Handle) fMyDataHandle); Inherited::Free(); }
By using a TemplateTempHandle, the member's destructor will automatically get called, so that the command doesn't have to manage the deletion manually or futz with coercions. The Free method is replaced with a destructor, and it doesn't have to do anything.
class TMyCommand : public TCommand { public: virtual ~TMyCommand(); ... TemplateTempHandle<MyDataHandle> fMyDataHandle; }; TMyCommand::~TMyCommand() { }
MacApp's venerable failure handling has been replaced with native C++ exception handling, which in many cases reduces the amount of code, and also helps make exception handling code more robust. Two examples are given in this section, one where a Try(fi) block can be safely removed by using auto pointers, and another where it is replaced with a native C++ try block.
Consider this example:
TMyClass::DoSomethingThatMayFail() { MAVolatileInit(MyDataHandle, myData, (MyDataHandle) NewPermHandle(sizeof(MyData)); MAVolatile(TMyCommand*, cmd); FailInfo fi; Try(fi) { cmd = new TMyCommand(myData); myData = NULL; cmd->IMyCommand(this); fi.Success(); } else { DisposeIfHandle((Handle) myData); FreeIfObject(cmd); fi.ReSignal(); } return cmd; }
This example has to manage a change in responsibility for a handle to data as well as cleaning up a command that's being constructed, and it has to worry about telling the compiler what local variables may get referenced when handling failure. Auto-pointers can be used to manage change in responsibility and cleaning up, and native C++ exceptions obviate the need to tell a compiler what is needed when handling an exception. Here is a rewritten version using auto-pointers without requiring a try block. In this case, the auto pointer method release() is used to tell the auto pointer it is no longer responsible for deleting the object when its destructor gets called which happens when it falls out of scope, or during an exception. The command pre-initialization code is a holdover from the earlier section on I method removal. Nice!
TMyClass::DoSomethingThatMayFail() { TemplateTempHandle<MyDataHandle> myData = (MyDataHandle) NewPermHandle(sizeof(MyData)); CAutoPtr_AC<TMyCommand> cmd; TDocument* itsDocument = NULL; this->DoSomeCommandPreInitialization(itsDocument); cmd = TH_new TMyCommand(myData, itsDocument); myData.release(); return cmd.release(); }
Sometimes it's still necessary to use a try block. Consider this example:
TMyClass::DoSomethingElseThatMayFail() { extern TFile* gMyFile; // implementation detail FailOSErr(gMyFile->OpenFile()); FailInfo fi; Try(fi) { ReadFromFile(gMyFile); fi.Success(); } else { gMyFile->CloseFile(); if (fi.error != noErr) PoseAlert(kMyAlert); fi.ReSignal(); } FailOSErr(gMyFile->CloseFile()); }
In this example, the objective is to ensure that a system resource (an open file) is closed properly upon failure, so the try block is retained, but rewritten using C++ exception keywords and exception class member functions. FailInfo and Success are no longer needed, and accessors theException.GetError() and theException.GetMessage() can be used in place of fi.error and fi.message.
To resignal an exception, use the 'throw' keyword. If you want debugging assistance code to be added to your catch block for your debug builds, add a call to the DoCatchMessage function.
TMyClass::DoSomethingElseThatMayFail() { extern TFile* gMyFile; // implementation detail ThrowIfOSErr_AC(gMyFile->OpenFile()); try { ReadFromFile(gMyFile); } catch (CException_AC& theException) { DoCatchMessage_AC(theException); gMyFile->CloseFile(); if (theException.GetError() != noErr) PoseAlert(kMyAlert); throw; } ThrowIfOSErr_AC(gMyFile->CloseFile()); }
Stack based objects have useful purposes other than just working magic during exceptions, they are also handy for tidying up temporary operations such as locking a handle.
For example, let's consider additional code added to the previous example (and turn the previous example into "etc."):
void TMyClass::DoSomeHandleWork() { extern Handle gMyHandle; // implementation detail // do some non-handle work short savedState = LockHandleHigh(gHandle); // do stuff with gHandle locked HSetState(gHandle, savedState); // do some more non-handle work }
Through the use of a temporary handle locking class whose destructor automatically unlocks the handle, we no longer have to remember to call cleanup code, and we can still decide when the handle should be unlocked again simply by carefully placing the braces:
void TMyClass::DoSomeHandleWork() { extern Handle gMyHandle; // implementation detail // do some non-handle work { // braces keep handle locked for shortest time CTempHandleLock_AC tempHandleLock(gHandle); // do stuff with gHandle locked } // do some more non handle work }
Iterator syntax has become templatized and somewhat more compact, following some commonly adopted conventions. Here is an example using a common iterator class that has already been defined by MacApp.
void TMyClass::DoSomeCommandPreInitialization(TDocument* itsDocument) { ... CDocumentIterator iter(this); for (TDocument* document = iter.FirstDocument(); iter.More(); document = iter.NextDocument()) { if (document->IsEnabled()) // do something with document } }
Because iterators are now templatized, iter.Current() is typed to the class being iterated, and (*iter) or iter-> can be used to reference the currently iterated object.
void TMyClass::DoSomeCommandPreInitialization(TDocument* itsDocument) { ... for (CDocumentIterator iter(this); iter.Current(); ++iter) { if (iter->IsEnabled()) // do something with iter.Current() } }
A TList is often used with a CObjectIterator without subclassing to iterate over its contents. Because the list doesn't know what type of object is being iterated, you may typically use type coercion in code where the object iterator is used. Here is a typical example.
void TMyClass::DoObjectIteration(TList* theList) { CObjectIterator iter(theList); for (TBlahClass* blah = (TBlahClass*) iter.FirstObject(); iter.More(); blah = (TBlahClass*) iter.NextObject()) { blah->DoSomething(); } }
The CObjectIterator class has been replaced with a TemplateArrayIterator_AC class, which implements modern C++ style iteration and does not require type coercion because it is templatized on the class being iterated.
void TMyClass:DoObjectIteration(CList_AC* theList) { for (TemplateArrayIterator_AC<TBlahClass*> iter(theList); iter; ++iter) { TBlahClass* blah = *iter; // no cast needed, iter is templated blah->DoSomething(); // or just iter->DoSomething(); } }
No matter how hard we try, sometimes dynamic casting is more convenient than a more elaborately-designed class hierarchy, and sometimes it is downright necessary. Here is an example of how dynamic casting may have been done in MacApp 3.3, although there are a number of other ways this could be done (for example, the MA_MEMBER macro could be used place of the class desc gunk, and that macro could still be used in Release 13, but it would not be needed in this example since we subsequently want a typesafe cast anyway and we can kill two birds with one stone using a single native C++ dynamic cast).
void TMyClass::DoSomeCommandPreInitialization(TDocument* itsDocument) { ... if ((document->GetClassDescDynamic())->DescendsFrom(&TMyDocument::fgClassDesc)) // can I safely cast this? { TMyDocument* myDoc = (TMyDocument*) document; ... } }
When the dynamic casting is performed using native C++ RTTI, the logic changes subtly from "can I safely cast this?" to "did the cast succeed?". Note that for places where you require that the cast succeed, you can call the dynamic_cast_AC macro, which adds a ThrowIfNULL_AC call after the cast.
void TMyClass::DoSomeCommandPreInitialization(TDocument* itsDocument) { ... TMyDocument* myDoc = dynamic_cast<TMyDocument*> (iter.Current()); if (myDoc) // did cast succeed? { ... } }
GridViews now support 32-bit grid coordinates, and by association, 32-bit grid selections. Although GridViews provided abstractions for the day this would happen in GridCoordinate, GridPoint, and GridRect, selections were explicitly region-based. Not any more. TGridViewDesignator, which replaces the use of regions, can now manage much larger selections.
Consider this example:
void TMyGridView::MyUpdateSelection() { // initialize things RgnHandle selectRgn = MakeNewRgn(); RgnHandle tmpRgn = MakeNewRgn(); // for each selected item, add it to the selection region for (short index = 1; index <= GetNumOfRows(); ++index) if (MyIsRowSelected(index)) { SetRectRgn(tmpRgn, 2, index, 3, index + 1); UnionRgn(selectRgn, tmpRgn, selectRgn); } // set the selection to the selection region calcalated above and clean up DisposeRgn(tmpRgn); Inherited::SetSelection(selectRgn, kDontExtend, kHighlight, kSelect); DisposeRgn(selectRgn); }
Note the mistaken assumption in using a short for row iteration. GridViews now use a long for GridCoordinate. By using GridCoordinate explicitly in your code, you are abstracted away from this implementation detail. Also watch for mistaken use of CRect when you mean to use GridRect. TGridViewDesignator handles memory allocation and deletion itself, so you get to concentrate on what is a selection. The methods map directly to the old region functions you may already be using.
void TMyGridView::MyUpdateSelection() { TGridViewDesignator selectRgn; TGridViewDesignator tmpRgn; // for each selected item, add it to the selection region for (GridCoordinate index = 1; index <= GetNumOfRows(); ++index) if (MyIsRowSelected(index)) { tmpRgn.Set(GridRect(2, index, 3, index+ 1)); selectRgn.Append(&tmpRgn); } // set the selection to the selection region calculated above Inherited::SetSelection(&selectRgn, kDontExtend, kHighlight, kSelect); }
Procedural view creation has changed a bit. Most view I method parameters have moved to the constructor, with the exception of two crucial parameters. This section explains how to cope with those two parameters. For information on handling the rest of the conversion from an I method to a constructor, see the command example in I Methods.
Consider this example:
void TMyGridView::MyEmbedSubView() { TMyView* view = new TMyView; view->IMyView(this, this->fDocument); // I-method embeds and associates with document }
In the new MacApp, the superview and document parameters did not migrate into the view constructors. This is because both require polymorphic method calls in order to complete embedding and association with a document. Hence, AddSubView must be called directly to embed the view. If any other polymorphic initialization is needed by the view or if you intend to associate the view with a document, HandlePostCreate must be called as well. This design allows you to fully instantiate the view hierarchy by making repeated calls to AddSubView, followed by a single call to HandlePostCreate on the topmost view in the hierarchy, which will perform post-initialization on all the subviews at once through iteration.
void TMyGridView::MyEmbedSubView() { TMyView* view = new TMyView; view->AddSubView(this); // embed view->HandlePostCreate(this->fDocument); // associate with document }
This example has not been completed yet.
Interfaces
typedef struct MyData { /* etc. */ } MyData, *MyDataPtr, **MyDataHandle; class TMyClass : public TObject { public: ... virtual void ReadFrom(TStream* itsStream); // override void DoSomeCommandPreInitialization(TDocument* itsDocument); void DoObjectIteration(TList* theList); void DoSomethingThatMayFail(); void DoSomethingElseThatMayFail(); void DoSomeHandleWork(); }; class TMyCommand : public TCommand { public: TMyCommand(MyDataHandle myDataHandle); virtual ~TMyCommand() {}; void IMyCommand(TMyClass* itsMyClass); virtual void Free(); ... MyDataHandle fMyDataHandle; }; class TMyGridView : public TGridView { public: ... Boolean MyIsRowSelected() { /* etc. */ } void MyUpdateSelection(); void MyEmbedSubView(); };
TMyCommand::TMyCommand(MyDataHandle myDataHandle) { fMyDataHandle = myDataHandle; } void TMyCommand::Free() { fMyDataHandle = (MyDataHandle) FreeIfObject((Handle) fMyDataHandle); Inherited::Free(); } TMyCommand::IMyCommand(TMyClass* itsMyClass) { TDocument* itsDocument = NULL; itsMyClass->DoSomeCommandPreInitialization(itsDocument); ICommand(cMyCommand, itsDocument, TRUE, TRUE, itsDocument); } void TMyClass::DoSomeCommandPreInitialization(TDocument* itsDocument) { // this is a hypothetical example that does a couple of things // in its own peculiar way (e.g., it is highly representative!). itsDocument = NULL; CDocumentIterator iter(this); for (TDocument* document = iter.FirstDocument(); iter.More(); document = iter.NextDocument()) { if (document->IsEnabled()) if ((document->GetClassDescDynamic())->DescendsFrom(&TMyDocument::fgClassDesc)) // can I safely cast this? { TMyDocument* myDoc = (TMyDocument*) document; if (myDoc->IsMyClassDocument(this)) // some imaginary test for contains-me-ness { itsDocument = myDoc; break; } } } } void TMyClass::DoObjectIteration(TList* theList) { CObjectIterator iter(theList); for (TBlahClass* blah = (TBlahClass*) iter.FirstObject(); iter.More(); blah = (TBlahClass*) iter.NextObject()) { blah->DoSomething(); } } void TMyClass::DoSomethingThatMayFail() { MAVolatileInit(MyDataHandle, myData, (MyDataHandle) NewPermHandle(sizeof(MyData))); MAVolatile(TMyCommand*, cmd); FailInfo fi; Try(fi) { cmd = new TMyCommand(myData); myData = NULL; cmd->IMyCommand(this); fi.Success(); } else { DisposeIfHandle((Handle) myData); FreeIfObject(cmd); fi.ReSignal(); } return cmd; } void TMyClass::DoSomethingElseThatMayFail() { extern TFile* gMyFile; // implementation detail FailOSErr(gMyFile->OpenFile()); FailInfo fi; Try(fi) { ReadFromFile(gMyFile); fi.Success(); } else { gMyFile->CloseFile(); if (fi.error != noErr) PoseAlert(kMyAlert); fi.ReSignal(); } FailOSErr(gMyFile->CloseFile()); } void TMyClass::DoSomeHandleWork() { extern Handle gMyHandle; // implementation detail // do some non-handle work short savedState = LockHandleHigh(gHandle); // do stuff with gHandle locked HSetState(gHandle, savedState); // do some more non-handle work } void TMyGridView::MyUpdateSelection() { // initialize things RgnHandle selectRgn = MakeNewRgn(); RgnHandle tmpRgn = MakeNewRgn(); // for each selected item, add it to the selection region for (short index = 1; index <= GetNumOfRows(); ++index) if (MyIsRowSelected(index)) { SetRectRgn(tmpRgn, 2, index, 3, index + 1); UnionRgn(selectRgn, tmpRgn, selectRgn); } // set the selection to the selection region calcalated above and clean up DisposeRgn(tmpRgn); Inherited::SetSelection(selectRgn, kDontExtend, kHighlight, kSelect); DisposeRgn(selectRgn); } void TMyGridView::MyEmbedSubView() { TMyView* view = new TMyView; view->IMyView(this, this->fDocument); // I-method embeds and associates with document }
Summary notes:
class TMyClass : public MStreamable_AC { public: ... virtual void ReadFrom(CStream_AC* itsStream); // override ... void DoSomeCommandPreInitialization(TDocument* itsDocument); void DoObjectIteration(CList_AC* theList); void DoSomethingThatMayFail(); void DoSomethingElseThatMayFail(); void DoSomeHandleWork(); }; class TMyCommand : public TCommand { public: TMyCommand(MyDataHandle myDataHandle, TDocument* itsDocument); virtual ~TMyCommand(); ... TemplateTempHandle<MyDataHandle> fMyDataHandle; }; class TMyGridView : public TGridView { public: ... bool MyIsRowSelected() const { /* etc. */ } void MyUpdateSelection(); };
Implementation changes:
TMyCommand::TMyCommand(MyDataHandle myDataHandle, TDocument* itsDocument) : TCommand(cMyCommand, itsDocument, true, true, itsDocument), fMyDataHandle(myDataHandle) { } TMyCommand::~TMyCommand() { } void TMyClass::DoSomeCommandPreInitialization(TDocument* itsDocument) { itsDocument = NULL; for (CDocumentIterator iter(this); iter.Current(); ++iter) { if (iter->IsEnabled()) { TMyDocument* myDoc = dynamic_cast<TMyDocument*> (iter.Current()); if (myDoc) // did cast succeed? if (myDoc->IsMyClassDocument(this)) // some imaginary test for contains-me-ness { itsDocument = myDoc; break; } } } } void TMyClass:DoObjectIteration(CList_AC* theList) { for (TemplateArrayIterator_AC<TBlahClass*> iter(theList); iter; ++iter) { TBlahClass* blah = *iter; // no cast needed, iter is templated blah->DoSomething(); // or just iter->DoSomething(); } } void TMyClass::DoSomethingThatMayFail() { TemplateTempHandle<MyDataHandle> myData = (MyDataHandle) NewPermHandle(sizeof(MyData)); CAutoPtr_AC<TMyCommand> cmd; TDocument* itsDocument = NULL; this->DoSomeCommandPreInitialization(itsDocument); cmd = TH_new TMyCommand(myData, itsDocument); myData.release(); return cmd.release(); } void TMyClass::DoSomethingElseThatMayFail() { extern TFile* gMyFile; // implementation detail extern Handle gHandle; // ditto ThrowIfOSErr_AC(gMyFile->OpenFile()); { // scoping braces keeps handle locked as little as possible try { ReadFromFile(gMyFile); // & do something with gHandle locked } catch (CException_AC& theException) { DoCatchMessage_AC(theException); gMyFile->CloseFile(); if (theException.GetError() != noErr) PoseAlert(kMyAlert); throw; } } ThrowIfOSErr_AC(gMyFile->CloseFile()); } void TMyClass::DoSomeHandleWork() { extern Handle gMyHandle; // implementation detail // do some non-handle work { CTempHandleLock_AC tempHandleLock(gHandle); // do stuff with gHandle locked } // do some more non handle work } void TMyGridView::MyUpdateSelection() { TGridViewDesignator selectRgn; TGridViewDesignator tmpRgn; // for each selected item, add it to the selection region for (GridCoordinate index = 1; index <= GetNumOfRows(); ++index) if (MyIsRowSelected(index)) { tmpRgn.Set(GridRect(2, index, 3, index+ 1)); selectRgn.Append(&tmpRgn); } // set the selection to the selection region calculated above Inherited::SetSelection(&selectRgn, kDontExtend, kHighlight, kSelect); } void TMyGridView::MyEmbedSubView() { TMyView* view = new TMyView; view->AddSubView(this); // embed view->HandlePostCreate(this->fDocument); // associate with document }