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

21

Good Code Style

We all learn from our mistakes. The trick is to learn from the mistakes of others

Anon

Symbian OS is designed to work well on devices with limited resources while still providing rich functionality. To balance the demand on system resources with their relative scarcity requires program code to be of high quality; in particular, robust, efficient and compact.

You can find advice on writing good-quality C++ code in any decent textbook on the subject. There are entire books which cover aspects of good programming style and there isn’t enough space in this book to go into too much detail. Instead, I’ve tried to focus on a few aspects that can improve your C++ code on Symbian OS. The chapter covers some of the main features of the Symbian OS coding standards and distils advice that developers working on Symbian OS have found to be most useful. While it concentrates on issues specific to Symbian OS, it also has some tips for general good programming style. I used some excellent books on C++ for background information when researching this chapter; those I recommend in particular are listed in the Bibliography.

21.1 Reduce the Size of Program Code

Space is at a premium on Symbian OS. Where possible, you should attempt to minimize the size of your program binary and its use of memory without unduly complicating your code. In previous chapters, I’ve already discussed some of the factors that may increase the size of your object code. For example, Chapter 5 mentioned that the use of the _L macro is deprecated and you should prefer to use _LIT for literal descriptors. The TRAP/TRAPD macros, which I discussed in Chapter 2, can also bloat program code. The following discussion describes some other factors to consider which may help you trim the size of your program binaries.

318

GOOD CODE STYLE

Take Care When Using inline

Beware of inlining functions, since you may end up bloating your program code while you are trying to speed it up. You’ll find a lot more about the hazards of inlining in Scott Meyers’ Effective C++.1 To summarize: the inline directive, when obeyed by a compiler, will replace a call to the inlined function with the body of the function, thus avoiding the overhead of a function call. However, this will increase the size of your code if the function code is anything other than trivial. (If the directive is ignored things may be even worse, since the compiler may generate multiple copies of your function code, while still making function calls to it, thus bloating your binary without any of the speed benefits from forgoing the function call.) On Symbian OS, limited memory resources typically mean that the overhead of a function call is preferable to the potential code bloat from a large section of inlined code.

Furthermore, inlined code is impossible to upgrade without breaking binary compatibility. If a client uses your function, it is compiled into their code, and any change you make to it later will force them to recompile, as I described in Chapter 18.

For both reasons, stick to the rule that you should prefer a function call and return over an inlined function unless the function is trivial, such as a simple ”getter” type function, shown below:

class TBook

{

public:

...

inline TInt Price() const {return (iPrice);};

private:

TInt iPrice;

};

Other candidates for inlining include trivial T class constructors and templated functions, particularly when using the thin template idiom discussed in Chapter 19.

Reuse Code

Code reuse has the benefit that you get to use tried and tested code. This is great if the solution fits your problem exactly, but how many times have you found that it doesn’t quite do what you want? Often you may find yourself writing code from scratch or copying and reworking code from a previous solution to solve your problem. Both courses are understandable if the prior solution doesn’t fit exactly, but it is wasteful of space to end up with two or more different solutions to a common problem.

1 See Item 33 of Effective C++: 50 specific ways to improve your programs and designs, details of which can be found in the Bibliography.

REDUCE THE SIZE OF PROGRAM CODE

319

Sometimes it may be possible to abstract the common code into a utility module which can be used by both solutions. Of course, this isn’t always possible, since you may not be able to modify the original source. If you cannot modify the original code, do you have to copy it for your solution, or can you use it and perform additional tasks to get the result you need? If you do have to copy and modify the code, try to copy only as much as is required, and make sure you adapt it where you need to, rewriting it completely if necessary so it is efficient for the task you are trying to achieve.

When writing a class, you should endeavor to make it easy to use and thus to reuse. Sometimes code is dismissed as being impossible to reuse because it’s so hard to work out what it’s doing or how best to use it. This is partly a problem with the language, because C++ is complex and it is easy to use it incorrectly. Chapter 20 discusses some of the ways to define a clear and efficient API, which will make your life easier in the testing, maintenance and documentation phases and will improve the chances that potential clients will adopt it, rather than duplicate effort and code by writing their own version.

Refactor Code

Martin Fowler describes the concept of code refactoring in his book

Refactoring: Improving the Design of Existing Code, details of which can be found in the Bibliography. As a module evolves, it may begin to lose its structure as new functions are added to enhance and extend the program. Even well-designed code, which initially performed a specific job well, becomes increasingly complex when modified to support additional requirements built on top of the original design. In theory, the program should be redesigned rather than simply extended. However, this usually doesn’t happen for numerous reasons, often related to the release schedule, which may be unprepared for redesign and reimplementation of code which is apparently fit-for-purpose.

Fowler suggests that refactoring techniques can be used as a trade-off between the long-term pain of additional code complexity and the shortterm pain of redesign, such as the introduction of new bugs and glitches. Refactoring doesn’t usually change the functionality of existing code but, rather, changes its internal structure to make it easier to work with as it evolves and is extended. The changes come through small steps, such as renaming methods, abstracting similar code from two classes into a separate shared superclass or moving member variables between classes when ownership changes become appropriate. The idea of refactoring is not to extend the code itself; this should be a distinctly separate operation to meet new requirements. But refactoring should take place whenever code is extended or bugs are fixed; it’s an ongoing process to reduce complexity and increase code reuse. You should make sure you have

320

GOOD CODE STYLE

good test code in place when you refactor and run the tests after each modification to ensure that you do not introduce regressive bugs into the code.

Remove Debug, Test and Logging Code in Release Builds

Chapter 16 discusses when assertion statements are appropriate in release builds; otherwise, any code you have added for debugging, testing and logging should be compiled out of release code using the appropriate macros. This allows you to minimize the overhead of diagnostic checking, in terms of runtime speed and code size, in production code.

21.2Use Heap Memory Carefully

Memory management is an important issue on Symbian OS, which provides an infrastructure to help catch memory leaks, use memory efficiently and test that code fails gracefully under low-memory conditions. I’ll discuss each of these further in this section. The issue of good memory management is so crucial to writing effective code on Symbian OS that you’ll find most chapters in this book touch on it to some extent. This section references those other chapters where relevant.

Prevent Memory Leaks

In Chapter 3, I described why you should use the cleanup stack and how to do so in order to handle leaves safely. The general rule to follow is that, when writing each line of code, you should ask yourself: ”Can this code leave? If so, will all the currently allocated resources be cleaned up?” By ensuring that all resources are destroyed in the event of a leave, you are preventing potential memory leaks when exceptions occur. This includes using the two-phase construction idiom for C classes, because a C++ constructor should never leave. I discussed this in more detail in Chapter 4.

You should, of course, ensure that all resources are cleaned up under normal conditions too. For example, every resource an object owns should be freed, as appropriate, in a destructor or cleanup function. It’s a common mistake to add a member variable to a class but to forget to add corresponding cleanup code to the destructor. I described how to check that your code doesn’t leak in Chapter 17.

Be Efficient

As well as preventing leaks, you should use memory efficiently to prevent wasting the limited resources available on a typical mobile phone. For

USE HEAP MEMORY CAREFULLY

321

example, you may want to consider whether the object size of your heapbased classes can be reduced. Possible slimming techniques include replacing a number of boolean flags with a bitfield and checking that you are using the Symbian OS container classes efficiently.2 You should also check that multiple copies of filenames, or other string data, are not copied unnecessarily between components in a module – perhaps passing them instead as reference parameters between components, where possible, to avoid duplication.

There are many other ways to reduce memory use, but it’s also sensible to consider how complex your code may become. There may be a degree of compromise between preserving the readability of your code and shaving off some extra bytes of memory consumption. As I’ve already mentioned, it’s important to remember that if your code is hard to understand, it is less likely to be reused. Potential clients may prefer to re-implement code rather than spend time figuring out how it works, and worrying about how they will maintain it.

By extension, you should prefer to use the classes provided by Symbian OS, rather than re-implement your own version of, for example, a string class. As I described in Chapters 5 and 6, the descriptor classes were carefully designed for memory efficiency. The Symbian OS classes have been written and maintained by those with expertise in dealing with restricted memory resources, and the classes have been extensively tried and tested as the OS has evolved. While the experience of writing your own utility classes and functions is irreplaceable, you get guaranteed efficiency and memory safety from using those provided by Symbian OS.

When allocating objects on the heap (and on the stack as described in the following section), the lifetime of the object is fundamental. On Symbian OS, programs may run for months and the user may rarely reset their phone. For this reason, objects should be cleaned up as soon as their useful lifetime ends – whether normally or because of an error condition. You should consider the lifetime of temporary objects allocated on the heap and delete them as soon as they have served their purpose. If you retain them longer than is necessary, the RAM consumption of the application may be higher than necessary.

Of course, there’s often a trade-off between memory usage and speed. For example, you may decide to conserve memory by creating an object

2 The RArray- and CArray-derived container classes take a granularity value on construction which is used to allocate space for the array in blocks, where the granularity indicates the number of objects in a block. Your choice of granularity must be considered carefully because, if you over-estimate the size of a block, the additional memory allocated will be wasted. For example, if a granularity of 20 is specified, but the array typically contains only five objects, then the space reserved for 15 objects will be wasted. Of course, you should also be careful not to under-estimate the granularity required, otherwise frequent reallocations will be necessary. Chapter 7 discusses the use of the Symbian OS container classes in more detail.

322

GOOD CODE STYLE

only when necessary, and destroying it when you’ve finished with it. However, if you need to use it more than once, you will reduce speed efficiency by duplicating the effort of instantiating it. On these occasions, individual circumstances will dictate whether you use the memory and cache the object or take the speed hit and create it only when you need it, destroying it afterwards. There is some middle ground, though: you may prefer not to instantiate the object until its first use and then cache it.

Once you have deleted any heap-based object, you should set the pointer to NULL3 to prevent any attempt to use it or delete it again. However, you don’t need to do this in destructor code: when member data is destroyed in a destructor, no code will be able to re-use it because it drops out of scope when the class instance is deleted.

In the following example, don’t worry too much about the details of class CSwipeCard or the two security objects it owns, iPassword and iPIN. However, consider the case where a CSwipeCard object is stored on the cleanup stack and ChangeSecurityDetailsL() is called upon it. First the two current authentication objects are deleted, then a function is called to generate new PIN and password objects. If this function leaves, the cleanup stack will destroy the CSwipeCard object. As you can see from the destructor, this will call delete on iPIN and iPassword – consider what would happen if I had not set each of these values to NULL. The destructor would call delete on objects that had already been destroyed, which isn’t a safe thing to do: the result is undefined, though in debug builds of Symbian OS it will certainly raise a panic (USER 44). Note that I’ve not set the pointers to NULL in the destructor – as I mentioned above, at this point the object is being destroyed, so no further access will be made to those pointers. It’s safe to call delete on a NULL pointer, so setting the deleted pointer to NULL after deletion prevents an unnecessary panic if the method which creates the new PIN and password objects leaves.

ChangeSecurityDetailsL() is also called from ConstructL(), at which point the iPIN and iPassword pointers are NULL (because the CBase parent class overloads operator new to zero-fill the object’s memory on creation, as I described in Chapter 1).

class CPIN; class CPassword;

class CSwipeCard : public CBase

{

3 As an aside: in debug builds of Symbian OS, when a heap cell is freed, the contents are set to 0xDE. If you are debugging and spot a pointer to 0xDEDEDEDE, the contents have been deleted but the pointer has not been NULLed. Any attempt to use this pointer will result in a panic.

USE HEAP MEMORY CAREFULLY

323

public:

CSwipeCard();

void ChangeSecurityDetailsL();

static CSwipeCard* NewLC(TInt aUserId); private:

CSwipeCard(TInt aUserId); void ConstructL();

void MakePasswordAndPINL(); private:

TInt iUserId;

CPIN* iPIN;

CPassword* iPassword; };

CSwipeCard::CSwipeCard(TInt aUserId) : iUserId(aUserId) {}

CSwipeCard:: CSwipeCard()

{

delete iPIN; // No need to NULL the pointers delete iPassword;

}

CSwipeCard* CSwipeCard::NewLC(TInt aUserId)

{

CSwipeCard* me = new (ELeave) CSwipeCard(aUserId);

CleanupStack::PushL(me);

me->ConstructL(); return (me);

}

void CSwipeCard::ConstructL()

{

ChangeSecurityDetailsL(); // Initializes the object

}

void CSwipeCard::ChangeSecurityDetailsL()

{

//Destroy original authentication object delete iPIN;

//Zero the pointer to prevent accidental re-use or double deletion iPIN = NULL;

delete iPassword; iPassword = NULL;

MakePasswordAndPINL(); // Create a new random PIN and password

}

An alternative, slightly more complex, implementation of ChangeSecurityDetailsL() could create the new PIN and password data in temporary descriptors before destroying the current iPIN and iPassword members. If either allocation fails, the state of the CSwipeCard object is retained – whereas in the implementation above, if MakePasswordAndPINL() leaves, the CSwipeCard object is left in an uninitialized state, having no password or PIN values set. The choice of implementation is usually affected by what is expected to happen if

ChangeSecurityDetailsL() leaves.

324 GOOD CODE STYLE

void CSwipeCard::ChangeSecurityDetailsL() // Alternative implementation

{// MakePasswordAndPINL() has been split into two for simplicity CPIN* tempPIN = MakePINL(); // Create a temporary PIN CleanupStack::PushL(tempPIN);

CPassword* tempPassword = MakePasswordL();

CleanupStack::PushL(tempPassword);

delete

iPIN;

//

No need to NULL

these, nothing can leave

delete

iPassword;

//

before they are

reset

iPIN = tempPIN; iPassword = tempPassword;

CleanupStack::Pop(2, tempPIN); // Owned by this (CSwipeCard) now

}

Finally in this section on memory efficiency, it’s worth mentioning that you should avoid allocating small objects on the heap. Each allocation is accompanied by a heap cell header which is four bytes in size, so it’s clear that allocating a single integer on the heap will waste space, as well as having an associated cost in terms of speed and heap fragmentation. If you do need a number of small heap-based objects, it is best to allocate a single large block of memory and use it to store objects within an array.

Degrade Gracefully – Leave if Memory Allocation Is Unsuccessful

You should code to anticipate low memory or other exceptional conditions. To save you coding a check that each and every heap allocation you request is successful, Chapter 2 describes the overload of operator new which leaves if there is insufficient heap memory for the allocation. Leaves are the preferred method of coping with exceptional conditions such as low memory on Symbian OS. When a memory failure occurs and causes your code to leave, you should have a higher level TRAP to ”catch” the failure and handle it gracefully (for example, by freeing some memory, closing the application, notifying the user, or whatever seems most appropriate).

Symbian OS runs on limited-resource mobile phones which will, at times, run out of memory. So you must prepare your code to cope under these conditions. To help you verify that your code performs safely under low memory conditions, Symbian OS provides a variety of test tools and macros that you can use in your own test code to simulate low memory conditions. I described these in detail in Chapter 17.

Check that Your Code Does not Leak Memory

I’ve already discussed how to prevent memory leaks, but, of course, this doesn’t mean that they won’t occur. It is important to test your code regularly to check that it is still leave-safe and that no leaks have been introduced by maintenance or refactoring. Symbian OS provides macros and test tools to test the heap integrity and to show up leaks when your