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

300

EXPOSE A COMPREHENSIVE AND COMPREHENSIBLE API

The quality of your API is important if your clients are to find it easy to understand and use. It should provide all the methods they need to perform the tasks for which the class is designed, but no more. You should design your interface to be as simple as possible, each member function having a distinct purpose and no two functions overlapping in what they perform. Try and limit the number of methods, too – if your class provides too many it can be confusing and difficult to work with. A powerful class definition has a set of functions where each has a clear purpose, making it straightforward, intuitive and attractive to reuse. The alternative might make a client think twice about using the class – if the functions are poorly declared, why trust the implementation? Besides the benefit of avoiding duplicating the implementation effort, with limited memory space on a typical Symbian OS phone, it’s in everyone’s interests to reuse code where possible.

Even if your own code is the intended client, no matter! Should you decide later to reuse the class, the time you spend making it convenient to use will pay off. By making the class simple to understand and use, and cutting out duplicated functionality, you’ll cut down your test and maintenance time too. Furthermore, a well-defined class makes documentation easier and that’s got to be a good thing.

20.1Class Layout

Before looking at details of good API design, let’s consider the aesthetics of your class definition. When defining a class, make it easy for your clients to find the information they need. The convention is to lay out Symbian OS classes with public methods at the top, then protected and private methods, following this with public data if there is any (and later in this chapter I’ll describe why there usually shouldn’t be), protected and private data. I tend to use the public, protected and private access specifiers more than once to split the class into logically related methods or data. It doesn’t have any effect on the compiled C++, and it makes the class definition simpler to navigate. For example:

class CMyExample : public CSomeBase

{

public: // Object instantiation and destruction static CMyExample* NewL();

static CMyExample* NewLC();

CMyExample();

public:

void PublicFunc1();

// Public functions, non virtual

...

public:

inline TBool Inline1();

// Inline (defined elsewhere)

...

public:

 

IMPORTC AND EXPORTC

301

 

 

virtual void VirtualFunc1(); // Public virtual

 

...

 

 

protected:

 

 

// Implementation of pure virtual base class functions

 

void DoProcessL();

 

...

 

 

// etc for other protected and private functions

 

private: // Construction methods, private for 2-phase construct

 

void ConstructL();

 

CMyExample();

 

 

...

 

 

private:

// Data can also be grouped, e.g. into that owned

 

CPointer* iData1; // by the class (which must be freed in the

 

...

// destructor) & that which does not need cleanup

 

};

 

 

 

 

 

20.2 IMPORTC and EXPORTC

Having declared your class, you should consider how your clients get access to it. If it will be used by code running in a separate executable (that is, DLL or EXE code compiled into a separate binary component) from the one in which you will deliver the class, you need to export functions from it. This makes the API ”public” to other modules by creating a .lib file, which contains the export table to be linked against by client code. There’s more about statically linked DLLs in Chapter 13.

Every function you wish to export should be marked in the class definition in the header file with IMPORT_C. Admittedly, this is slightly confusing, until you consider that the client will be including the header file, so they are effectively ”importing” your function definition into their code module. You should mark the corresponding function definition in the .cpp file with EXPORT_C. The number of functions marked IMPORT_C in the header files must match the number marked EXPORT_C in the source.

Here’s an example of the use of the macros, which will doubtless look familiar from class definitions in the header files supplied by whichever Symbian OS SDK you use.

class CMyExample : public CSomeBase

{

public:

IMPORT_C static CMyExample* NewL(); public:

IMPORT_C void Foo();

...

};

EXPORT_C CMyExample* CMyExample::NewL() {...}

302

EXPOSE A COMPREHENSIVE AND COMPREHENSIBLE API

EXPORT_C void CMyExample::Foo()

{...}

The rules as to which functions you need to export are quite simple. Firstly, you must never export an inline function. There’s simply no need to do so! As I described, IMPORT_C and EXPORT_C add functions to the export table to make them accessible to components linking against the library. However, the code of an inline function is, by definition, already accessible to the client, since it is declared within the header file. The compiler interprets the inline directive by adding the code directly into the client code wherever it calls it. In fact, if you export an inline function, you force all DLLs which include the header file to export it too, because the compiler will generate an out-of-line copy of the function in every object file which uses it. You’ll find a warning about the use of inline directives except for the most trivial of functions in Chapters 18 and 21.

Only functions which need to be used outside a DLL should be exported. When you use IMPORT_C and EXPORT_C on a function, it adds an entry to the export table. If the function is private to the class and can never be accessed by client code, exporting it merely adds it to the export table unnecessarily.

Private functions should only be exported if:

they are virtual (I’ll discuss when to export virtual functions shortly)

they are called by a public inline function (this is clear when you consider that the body of the inline function will be added to client code, which means that, in effect, it makes a direct call to the private function).

Similarly, a protected function should only be exported if:

it is called by an inline function, as described above

it is designed to be called by a derived class, which may be implemented in another DLL

it is virtual.

All virtual functions, public, protected or private, should be exported, since they may be re-implemented by a derived class in another code module. Any class which has virtual functions must also export a constructor, even if it is empty.

The one case where you should not export a virtual function is if it is pure virtual. This is obvious when you consider that there is generally no implementation code for a pure virtual function, so there is no code

PARAMETERS AND RETURN VALUES

303

to export. As long as a deriving class has access to the virtual function table for your base class, it is aware of the pure virtual function and is forced to implement it. In the rare cases where a pure virtual function has a function body, it must be exported.

The virtual function table for a class is created and updated by the constructor of the base class and any intermediate classes. If you don’t export a constructor, when a separate code module comes to inherit from your class, the derived constructor will be unable to access the default constructor of your class in order to generate the virtual function table. This is why any class which has virtual functions must export a constructor. Remember the rule that you must not export inline functions? Since you’re exporting the constructor, you must implement it in your source module, even if it’s empty, rather than inline it. A good example of this is class CBase (defined in e32base.h), which defines and exports a protected default constructor.

The rules of exporting functions are as follows:

never export an inline function

only those non-virtual functions which need to be used outside a DLL should be exported

private functions should only be exported if they are called by a public inline function

protected functions should be exported if they are called by

a public inline function or if they are likely to be called by a derived class which may be implemented in another code module

all virtual functions, public, protected or private, should be exported, since they may be re-implemented in a separate module

pure virtual functions should not be exported unless they contain code

any class which has virtual functions must also export a (noninlined) constructor, even if it is empty.

20.3Parameters and Return Values

Let’s move on to think about the definitions of class methods in terms of parameters and return values. I’ll compare passing and returning values

304

EXPOSE A COMPREHENSIVE AND COMPREHENSIBLE API

by value, reference and pointer, and state a few general guidelines for good C++ practice on Symbian OS.

Pass and Return by Value

void PassByValue(TExample aParameter);

TExample ReturnAValue();

Passing a parameter by value, or returning a value, takes a copy of the argument or final return value. This can potentially be expensive, since it invokes the copy constructor for the object (and any objects it encapsulates) as well as using additional stack space for the copy. When the object passed in or returned goes out of scope, its destructor is called. This is certainly less efficient than passing a reference or pointer to the object as it currently exists, and you should avoid it for large or complex objects such as descriptors.1 However, it is insignificant for small objects (say less than eight bytes) and the built-in types (TBool, TText, TInt, TUint and TReal). Of course, by taking a copy of the parameter, the original is left unchanged, so a parameter passed by value is most definitely a constant input-only parameter.

You may even choose to return an object by const value in some cases. This prevents cases of assignment to the return value – which is illegal for methods that return the built-in types. You should strive to make your own types behave like built-in types and may choose to use a const return value to enforce this behavior when you are returning an instance of your class by value.

Pass and Return by const Reference

void PassByConstRef(const TExample& aParameter);

const TExample& ReturnAConstRef();

Passing an object by const reference prevents the parameter from being modified and should be used for constant input parameters larger than the built-in types. Equally, for efficiency reasons, returning a value as a constant reference should be used in preference to taking a copy of a larger object to return it by value. You must be careful when returning an object by reference, whether it is modifiable or constant, because the caller of the function and the function itself must agree on the

1 When passing objects by reference or pointer, a 32-bit pointer value is transferred. This minimal memory requirement is fixed, regardless of the type to which it points. Internally, a reference is implemented as a pointer, with additional syntax to remove the inconvenience of indirection.

PARAMETERS AND RETURN VALUES

305

lifetime of the object which is referenced. I’ll discuss this further in the next section.

Pass and Return by Reference

void PassByRef(TExample& aParameter);

TExample& ReturnARef();

Passing an object by reference allows it to be modified; it is, in effect, an output or input/output parameter. This is useful for both larger objects and the built-in types, and is a common alternative to returning an object by value.

Returning a reference is commonly seen to allow read/write access, for example to a heap-based object for which ownership is not transferred. Returning a reference passes access to an existing object to the caller; of course, the lifetime of this object must extend beyond the scope of the function.

Consider the following example:

class TColor

{

public:

TColor(TInt aRed, TInt aGreen, TInt aBlue);

TColor& AddColor(TInt aRed, TInt aGreen, TInt aBlue); //... functions omitted for clarity

private:

TInt iRed; TInt iGreen; TInt iBlue; };

TColor::TColor(TInt aRed, TInt aGreen, TInt aBlue) : iRed(aRed), iGreen(aGreen), iBlue(aBlue)

{}

TColor& TColor::AddColor(TInt aRed, TInt aGreen, TInt aBlue) {// Incorrect implementation leads to undefined behavior TColor newColor(aRed+iRed, iGreen+aGreen, iBlue+aBlue); return (newColor); // Whoops, new color goes out of scope

}

TColor prettyPink(250, 180, 250);

TColor differentColor = prettyPink.AddColor(5, 10, 5);

The AddColor() method returns a reference to newColor, a local stack-based object which ceases to exist outside the scope of AddColor(). The result is undefined, and some compilers flag this as an error. If your compiler does let you build and run the code, it may even succeed, since different compilers allow temporary variables different life spans. In circumstances like these, that’s what ”undefined” means. What

306

EXPOSE A COMPREHENSIVE AND COMPREHENSIBLE API

works with one compiler will doubtless fail with another and you should avoid any reliance upon compiler-specific behavior.

The previous code is a classic example of knowing that it’s generally preferable to return objects larger than the built-in types by reference rather than by value, but applying the rule where it is not actually valid to do so. An equally invalid solution would be to create the return value on the heap instead of the stack, to avoid it being de-scoped, and then return a reference to the heap object. For example, here’s another incorrect implementation of adding two colors, this time in a leaving function.

TColor& TColor::AddColorL(TInt aRed, TInt aGreen, TInt aBlue) {// Incorrect implementation leads to a memory leak

TColor* newColor = new (ELeave) TColor(aRed+iRed, iGreen+aGreen, iBlue+aBlue);

return (*newColor);

}

The newColor object does at least exist, but who is going to delete it? By returning a reference, the method gives no indication that the ownership of newColor is being transferred to the method’s caller. Without clear documentation, the caller wouldn’t know that they’re supposed to delete the return value of AddColorL() when they’ve finished with it.

TColor prettyPink(250, 180, 250);

TColor differentColor = prettyPink.AddColor(5, 10, 5);

...

delete &differentColor; // Nasty. It’s unusual to delete a reference!

This just isn’t the done thing with functions which return objects by reference! Even if most of your callers read the documentation and don’t object heartily to this kind of behavior, it only takes one less observant caller to leak memory.

Of course, there is an expense incurred in return by value, namely the copy constructor of the object returned and, later, its destructor. But in some cases, such as that described above, you have to accept that (and in some cases the copy constructor and destructor are trivial so it’s not an issue). In addition, the compiler may be able to optimize the code (say, using the named return value optimization) so the price you pay for intuitive, memory-safe and well-behaved code is often a small one. Here’s the final, correct implementation of AddColor(), which returns TColor by value:

TColor TColor::AddColor(TInt aRed, TInt aGreen, TInt aBlue)

{

TColor newColor(aRed+iRed, iGreen+aGreen, iBlue+aBlue); return (newColor); // Correct, return by value

}

PARAMETERS AND RETURN VALUES

307

If you return a reference to an object which is local to the scope of your function, the object will be released back to the stack when your function returns, and the return value will not reference a valid object. In general, if an object doesn’t exist when the function that returns it is called, you can only return it by reference if some other object, whose life-time extends beyond the end of that function, has ownership of it.

Pass and Return by const Pointer

void PassByConstPtr(const CExample* aParameter);

const CExample* ReturnAConstPtr();

Passing or returning a constant pointer is similar to using a constant reference in that no copy is taken and the recipient cannot change the object.2 It’s useful to return a pointer when returning access to a C++ array, or where an uninitialized parameter or return value is meaningful. There’s no such thing as an uninitialized reference, so returning a NULL pointer is the only valid way of indicating ”no object” in a return value. However, you might consider overloading a method to take an additional constant reference input parameter. This means that you can use a const reference for cases where the input parameter can be supplied, and call the overloaded method which doesn’t take any input parameter when no input is supplied. For example:

// Take a const pointer (aInput may be NULL)

void StartL(const CClanger* aInput, const TDesC8& aSettings);

//Or alternatively overload to take a const reference when it is valid

//Use this overload in cases where aInput is NULL

void StartL(const TDesC8& aSettings);

// Use this overload in cases where aInput!=NULL

void StartL(const CClanger& aInput, const TDesC8& aSettings);

The benefit of using a reference over a pointer is that it is always guaranteed to refer to an object. In the first overload, StartL() would have to implement a check to see whether aInput points to an object before dereferencing it, in case it is NULL. This is unnecessary in the third overload, where aInput is guaranteed to refer to an object.

2 The constness of the pointer can, of course, be cast away using const_cast <CExample*>(aParameter), but you wouldn’t do that without good reason, would you?

308

EXPOSE A COMPREHENSIVE AND COMPREHENSIBLE API

Pass and Return by Pointer

void PassByPtr(CExample* aParameter);

CExample* ReturnAPtr();

Passing or returning a pointer allows the contents to be modified by the recipient. As with const pointers, you should pass or return a pointer if it can be NULL, if it points into a C++ array or if you are transferring ownership of an object to a caller. On Symbian OS, passing or returning a pointer often indicates a transfer of ownership and it’s preferable not to return a pointer if you are not transferring ownership (since you must then document clearly to callers that they should not delete it when they’ve finished with it). In fact, you should prefer to pass by reference or return a reference rather than use pointers except for the reasons described.

All else being equal, a reference can be more efficient than a pointer when the compiler must maintain a NULL pointer through a conversion. Consider the following class and pseudo-code:

class CSoupDragon : public CBase, public MDragon {...};

CSoupDragon* soupDragon;

MDragon* dragon;

...

dragon = soupDragon; // soupDragon may be NULL

For the conversion between CSoupDragon and MDragon, the compiler must add sizeof(CBase) to the soupDragon pointer in all cases, except where soupDragon is NULL, whereupon it must continue to be NULL rather than point incorrectly at the address which is the equivalent of sizeof(CBase). Thus the compiler must effect the following:

dragon = (MDragon*)(soupDragon ? (TUint8*)soupDragon+sizeof(CBase) :

NULL);

For a conversion which involves references rather than pointers, the test is unnecessary, since a reference must always refer to an object.

For any of your code that receives a pointer return value, or if you implement methods that take pointer parameters, you should consider the implications of receiving a NULL pointer. If an uninitialized value is incorrect, i.e. a programming error, you should use an assertion statement to verify that it is valid. The use of assertions for ”defensive” programming is discussed further in Chapter 16.