Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Symbian OS Explained - Effective C++ Programming For Smartphones (2005) [eng].pdf
Скачиваний:
70
Добавлен:
16.08.2013
Размер:
2.62 Mб
Скачать

CONSTRUCTORS AND DESTRUCTORS

17

...

inline TAny* operator new(TUint aSize, TLeave);

// e32std.inl

inline TAny* operator new(TUint aSize, TLeave) {return User::AllocL(aSize);}

Symbian OS has overloaded the global operator new to take a TLeave parameter. This overload leaves if memory is unavailable on the heap.

If a leaving function which allocates an object doesn’t leave, the allocation was successful and there is no need to check the result further.

2.3 Constructors and Destructors

Before moving on to talk further about how to call leaving functions, let’s consider which functions should not leave. Quite simply, neither a constructor nor a destructor should leave, since doing so would potentially leak memory and place the object upon which it is invoked in an indeterminate state.

Chapter 4 will discuss this in more detail, but essentially, if a constructor can fail, say, through lack of the resources necessary to create or initialize the object, you must remove the code that may leave from the constructor and use the two-phase construction idiom instead.

Likewise, a leave should not occur in a destructor or in cleanup code. One particular reason for this is that a destructor could itself be called as part of cleanup following a leave and a further leave at this point would be undesirable, if nothing else because it would mask the initial reason for the leave. More obviously, a leave part-way through a destructor will leave the object destruction incomplete which may leak its resources.

If a destructor must call a leaving function, it may potentially be trapped and the leave code discarded, although this causes problems when testing out of memory leaves using the debug macros described in Chapter 17. Generally, it’s preferable to have a separate leaving function, which can be called before destruction, to perform actions that may fail and provide the caller an opportunity to deal with the problem before finally destroying the object. Typically these would be functions such as

CommitL() or FreeResourceL().

18

LEAVES: SYMBIAN OS EXCEPTIONS

Constructors and destructors must not leave.

2.4 Working with Leaving Functions

Let’s look at the practicalities of working with leaves. Below is an example of a call to a leaving function. You’ll notice that there is no need to check that ironChicken is initialized before using it, since CTestClass::NewL() would have left if any failure had occurred.

void FunctionMayLeaveL()

{

//Allocates ironChicken on the heap CTestClass* ironChicken = CTestClass::NewL();

//If NewL() didn’t leave, ironChicken was allocated successfully ironChicken->FunctionDoesNotLeave();

delete ironChicken;

}

If the CTestClass::NewL() function leaves for some reason, it is the responsibility of that function to release any memory already allocated as part of the function. If successful, the function allocates, initializes and returns a heap-based object (NewL() functions and twophase construction are discussed in more detail in Chapter 4). In the code above, a call to a non-leaving function follows, but consider the implications if a leaving function was called instead. For example:

void UnsafeFunctionL()

{

// Allocates test on the heap CTestClass* test = CTestClass::NewL();

test->FunctionMayLeaveL(); // Unsafe – a potential memory leak! delete test;

}

This is unsafe. Memory is allocated on the heap in the call to CTestClass::NewL(), but the following function may leave. Should this occur test will not be deallocated; consequently the function has the potential to leak memory. In a scenario such as this, you should push the heap object onto the cleanup stack, which will delete it should a leave occur. The cleanup stack is described more fully in Chapter 3.

While heap variables referred to only by local variables may be orphaned in this way, member variables will not suffer a similar fate

WORKING WITH LEAVING FUNCTIONS

19

(unless their destructor neglects to delete them when it is called at some later point). Thus the following code is safe:

void CTestClass::SafeFunctionL()

{

iMember = CClangerClass::NewL(); // Allocates a heap member

FunctionMayLeaveL();

// Safe

}

Note that the CTestClass object (pointed to by ”this” in CTestClass::SafeFunctionL()) is not deleted in the event of a leave. The heap-based iMember is stored safely as a pointer member variable, to be deleted at a later stage with the rest of the object, through the class destructor.

I’ve shown that you must prevent leaks from the potential orphaning of heap-based local variables, but what about cleanup of stack variables if a leave occurs? The leave mechanism simply deallocates objects on the stack – it does not call any destructors they have defined as it does so, unlike a C++ throw. Stack objects that own a resource which must be deallocated, or otherwise ”released” as part of destruction, would leak that resource in the event of a leave. Classes which are intended to be used on the stack must not need a destructor.

This is the reason why Symbian OS has a class naming convention which clearly defines the allowed usage of a class (described in Chapter 1). The only classes which may be instantiated and used safely on the stack are T classes, which the Symbian OS naming convention dictates must not have a destructor, and R classes, which do not have a destructor but use Close(), or a similar method, to free the associated resource. The cleanup stack must be used to ensure that this method is called in the event of a leave – I’ll discuss how to do so in the next chapter.

class TMyClass

{

public:

TMyClass(TInt aValue); private:

TInt iValue; };

void AnotherSafeFunctionL()

{

TInt localInteger = 1; // Leave-safe (built-in type)

FunctionMayLeaveL(localInteger);

TMyClass localObject(localInteger); // Leave-safe object AnotherPotentialLeaverL(localObject);

}

Let’s consider what happens if you happen to have a local variable, an object of a T class, on the heap. In a leaving function, you

20

LEAVES: SYMBIAN OS EXCEPTIONS

still need to protect the heap memory from being orphaned by a leave but the object itself has no destructor. You’ll recall that I mentioned earlier that the cleanup stack performs both destruction and deallocation upon the objects in its care in the event of a leave. Well, that’s true for objects of class types which have destructors, but for T class objects, it simply deallocates the memory. There’s more about this in Chapter 3.

void AnotherFunctionL()

{

TMyClass* localObject = new (ELeave) TMyClass();

// Make localObject leave-safe using the cleanup stack CleanupStack::PushL(localObject); AnotherPotentialLeaverL(localObject); CleanupStack::PopAndDestroy(localObject);

}

2.5 Trapping a Leave Using TRAP and TRAPD

Symbian OS provides two macros, TRAP and TRAPD, to trap a leave. The macros differ only in that TRAPD declares a variable in which the leave error code is returned, while the program code itself must declare a variable before calling TRAP. Thus the following statement:

TRAPD(result, MayLeaveL());

if (KErrNone!=result) // See footnote 2

{

// Handle error

}

2 Throughout this text, you’ll notice that I prefer to use ”back to front” comparisons in my if statements to prevent accidentally typing only a single =, which is valid C++ but isn’t at all what is intended. Take the following example of the unforeseen consequences that can arise from this bug, which is often difficult to spot unless you have a helpful compiler that warns you about it.

TInt ContrivedFunction()

{ // The bug in this function means it will always return 0 for (TInt index = 0; index < KContrivedValue; index++)

{

TInt calculatedValue = DoSomeComplexProcessing(index); // This assignment always returns true

if (calculatedValue=KAnticipatedResult) return (index);

}

return (KErrNotFound);

}

However, not everybody likes this style of coding. If you prefer not to use this technique, it pays to compile with a high warning level and pay attention to any resulting warnings.

TRAPPING A LEAVE USING TRAP AND TRAPD

21

is equivalent to:

TInt result;

TRAP(result, MayLeaveL());

if (KErrNone!=result)

{

// Handle error

}

...

You should beware of nesting TRAPD macros and using the same variable name, as in the following snippet:

TRAPD(result, MayLeaveL())

if (KErrNone==result)

{

TRAPD(result, MayAlsoLeaveL())

}

...

User::LeaveIfError(result);

In the example, two TInt result variables are declared, one for each TRAPD statement. The scope of the second result macro is bounded by the curly brackets that enclose it. Thus any leave code assigned to the second result variable, from the call to MayAlsoLeaveL(), is discarded on exiting the bounding brackets. The User::LeaveIfError() call thus only tests the leave code from the MayLeaveL() call, which is unlikely to be what the code intended. To ensure both values are tested, the second TRAPD should be replaced with a TRAP – thus reusing the TInt result declared by the initial TRAPD.

If a leave occurs inside the MayLeaveL() function, which is executed inside the harness, the program control will return immediately to the trap harness macro. The variable result will contain the error code associated with the leave (i.e. that passed as a parameter to the User::Leave() system function) or will be KErrNone if no leave occurred.

Any functions called by MayLeaveL() are executed within the trap harness, and so on recursively, and any leave that occurs during the execution of MayLeaveL() is trapped, returning the error code into result. Alternatively, TRAP macros can be nested to catch and handle leaves at different levels of the code, where they can best be dealt with. I’ll discuss the runtime cost of using trap harnesses shortly, but if you find yourself using the TRAP macros several times in one function, or nesting a series of them, you may want to consider whether you can omit trapping all the leaving functions except at the top level, or change the layout of the code.

For example, there may be a good reason why the following function must not leave but needs to call a number of functions which may leave.

22

LEAVES: SYMBIAN OS EXCEPTIONS

At first sight, it might seem straightforward enough simply to put each call in a trap harness.

TInt MyNonLeavingFunction()

{

TRAPD(result, FunctionMayLeaveL()); if (KErrNone==result)

TRAP(result,AnotherFunctionWhichMayLeaveL()); if (KErrNone==result)

TRAP(PotentialLeaverL());

// Handle any error if necessary return (result);

}

However, each TRAP has an impact in terms of executable size and execution speed. Both entry to and exit of a TRAP macro result in kernel executive calls3 (TTrap::Trap() and TTrap::UnTrap()) being made. In addition, a struct is allocated at runtime to hold the current contents of the stack in order to return to that state if unwinding proves necessary in the event of a leave. Of course, use of the macro itself will also create additional inlined code, though it is probably insignificant in comparison. The combination of these factors does make a TRAP quite an expensive way of managing a leave. You should attempt to minimize the number of TRAPs you use in your code, where possible, either by allowing the leave to propagate to higher-level code or by making an adjustment similar to the following:

MyNonLeavingFunction()

{

TRAPD(result, MyLeavingFunctionL()); // Handle any error if necessary return (result);

}

void MyLeavingFunctionL()

{

FunctionMayLeaveL();

AnotherFunctionWhichMayLeaveL();

PotentialLeaverL();

}

Of course, code is rarely as trivial as this example and you should beware of losing relevant error information. If a number of calls to potential leaving functions are packaged together as above and a leave

3 A kernel executive call is made by user-side code to allow it to enter processor privileged mode in order to access kernel resources in a controlled manner. Control is switched to the kernel executive, and the processor is switched to supervisor mode, within the context of the calling thread.

TRAPPING A LEAVE USING TRAP AND TRAPD

23

code is returned from a call to the package of functions, it will not be clear which function left.

Every program (even a simple ”hello world” application) must have at least one TRAP, if only at the topmost level, to catch any leaves that are not trapped elsewhere. If you are an application writer you don’t need to worry about this, though, because the application framework provides a TRAP.

When testing any code that may leave, you should test both paths of execution, that is, for a successful call and for a call that leaves as a result of each of the exceptional conditions you expect to handle, for example, low memory conditions, failure to write to a file, etc. The Symbian OS macros to simulate such conditions are described in Chapter 17.

I mentioned earlier that, under normal circumstances, you shouldn’t implement functions which both return an error and have the potential to leave. For example, consider a function, OpenFileObjectL(), that instantiates an object (CFileObject) which must be initialized with an open file handle. The implementation of this object may be in a separate library, the source code for which may not be available to the writer of this code.

TInt OpenFileObjectL(CFileObject*& aFileObject)

{// File server session initialization is omitted for clarity

... // (handle is leave-safe) RFile file;

TInt error = file.Open(...); if (KErrNone==error)

{

CleanupClosePushL(file); // Makes handle leave-safe // Propagates leaves from CFileObject

aFileObject = CFileObject::NewL(file); CleanupStack::Pop(&file); // Owned by aFileObject now

}

return error; // Returns any error from RFile::Open()

}

When a caller comes to use OpenFileObjectL(), they’ll find that this function is more complex than it needs to be. At best, they may find themselves writing code something like the following:

void ClientFunctionL()

{// Pass all errors up to be handled by a higher-level TRAP harness CFileObject* fileObject = NULL;

TInt errorCode=OpenFileObjectL(); if (KErrNone!=errorCode)

{

User::Leave(errorCode);

}

...

}

24

LEAVES: SYMBIAN OS EXCEPTIONS

Or they may use a TRAP to catch any leaves and return them as errors:

TInt ClientFunction()

{

CFileObject* fileObject = NULL;

TInt errorCode;

TRAPD(r, errorCode=OpenFileObjectL()); if (KErrNone!=r)

return (r);

if (KErrNone!=errorCode) return (errorCode);

...

}

Neither of these options is very attractive. Furthermore, if the client function can actually handle some of the errors at that point in the code, it becomes even messier. Should it differentiate between the two methods of propagating an error? And if so, how? Are the leaves in some way different to the errors returned? Could the same error sometimes be returned as an error value (say KErrNotFound from RFile::Open()) and sometimes as a leave code (say a leave from CFileObject::NewL() which attempts to read the contents of the file and finds it is missing some vital configuration data)?

In effect, this approach requires you to document the function clearly to allow callers to use it correctly and handle errors or exceptions as they choose. Maybe you can guarantee that you, or whoever takes responsibility for maintaining this function, keeps the documentation up to date. But you are returning an error directly from one, separate, component (class RFile) and a leave code from another (class CFileObject). Even if you know exactly what errors and leave codes each will use and are prepared to document them for callers of OpenFileObjectL(), you cannot guarantee that their error handling will not change.

It is preferable to restrict the code either to leaving or to returning an error. First, consider an implementation that leaves under all circumstances (notice that I’ve changed the signature to return the CFileObject rather than pass it as a reference-to-pointer parameter).

CFileObject* LeavingExampleL()

{

RFile file;

User::LeaveIfError(file.Open(...));

return (CFileObject::NewL(file));

}

The implementation of the function is certainly smaller and less complex. If the calling code can handle some errors, then it can TRAP the method and switch on the error value returned in order to distinguish between the exceptional case and the non-exceptional case.

TRAPPING A LEAVE USING TRAP AND TRAPD

25

void ClientFunctionL()

{

CFileObject* fileObject = NULL;

TRAPD(r, fileObject = LeavingExampleL()); switch (r)

{

case (KErrNoMemory):

... // Free up some memory and try again...

break;

...

default:

User::Leave(err);

break;

}

}

Otherwise it can just call the function directly and allow a higher-level trap handler to handle all the leave codes:

CFileObject* fileObject = LeavingExampleL();

As an alternative to leaving on all errors, the function to instantiate a CFileObject could TRAP the call to CFileObject::NewL() and instead return an error for all failures:

TInt Example(CFileObject*& aFileObject)

{

RFile file;

TInt error = file.Open(...); if (error == KErrNone)

{

TRAP(error, aFileObject = CThing::NewL(file));

}

return error;

}

Again, if the calling code can handle some errors, it can switch on the return code; otherwise, it can call the function inside User::LeaveIfError() to pass all failures up to a TRAP harness in higher-level code.

CFileObject* fileObject = NULL;

User::LeaveIfError(Example(fileObject));

Which of the two implementations is preferable? Well, the leaving version is smaller and simpler and, as Martin Fowler discusses in Refactoring: Improving the Design of Existing Code (see Bibliography for further details), the use of exceptions clearly separates normal processing from error processing. Furthermore, the use of the alternative, error-returning function always incurs the overhead of a