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

PREVENTING COMPATIBILITY BREAKS

281

client component which was dependent on the original version of the library is not affected by the addition of a function to the end of the export list of the library. Thus the change can be said to be binary-compatible and backward-compatible (as well as source-compatible).

However, if the addition of a new function to the class causes the ordinals of the exported functions to be reordered, the change is not binary-compatible, although it continues to be source-compatible. Dependent code must be recompiled, because otherwise it would use the original, now invalid, ordinal numbers to identify the exports.

Of course, when we discuss the need for compatibility, this is with relation to the API which has been published to external components. You may make as many internally-incompatible changes to your code as you need to, since it is only your components that are affected. The restrictions I’ll discuss in the rest of this chapter apply to changes which affect published, public APIs.

Let’s move on to consider some of the practical aspects of code, to help you understand the kind of source code changes that can affect compatibility. I’ll list some guidelines about which code changes do and do not affect the binary layout at a class or DLL level. These derive from an understanding of the way a compiler interprets the C++ standard. It is only by deriving a set of rules in this way that we can be confident as to which changes can be safely applied without breaking dependent code.

Many of the following guidelines are based on whether class methods or member data are ”visible” to dependent code. This assumes that calling code gains visibility through legitimate means, that is, through exported header files, using standard object-oriented C++ to access and manipulate the methods and data encapsulated in C++ objects.

18.4 Preventing Compatibility Breaks

Do Not Change the Size of a Class Object

You should not change the size of a class object (e.g. by adding or removing data) unless you can guarantee that:

your class is not externally derivable

the only code that allocates an object resides within your component, or it has a non-public constructor that prevents it from being created on the stack

this has significant implications for binary compatibility, because the ordinals of exported functions must not be changed between one release of a DLL and another (otherwise code which originally used the old DLL will not be able to locate the functions it needs in the new version of the DLL).

282

COMPATIBILITY

• the class has a virtual destructor.

The reasons for this rule are fairly straightforward. First, the size of memory required for an object to be allocated on the stack is determined for each component at build time, and to change the size of an object would affect previously compiled client code, unless the client is guaranteed to instantiate the object only on the heap, say by using a NewL() factory function.

Additionally, access to data members within an object occurs through an offset from the this pointer. If the class size is changed, say by adding a data member, the offsets of the data members of derived classes are rendered invalid.

To ensure that an object of a class cannot be derived or instantiated except by members (or friends) of the class, it should have private, non-inline and non-exported constructors. It is not sufficient simply not to declare any constructors for the class, since the compiler will then generate an implicit, public default constructor. On Symbian OS, all C classes derive from CBase, which defines a protected default constructor, and prevents the compiler from generating an implicit version.2 If your class needs a default constructor, you should simply define it as private and implement it in source, or at least, not inline where it is publicly accessible.

Here’s a definition for a typical, non-derivable class:

Class CNonDerivable : public CBase

{

public:

//Factory - instantiates the object via ConstructL() IMPORT_C static CNonDerivable* NewL();

//Virtual destructor, inherited from CBase

IMPORT_C virtual CNonDerivable(); public:

... // Omitted for clarity – public exports & virtual functions private:

// constructor is private, not protected, to prevent subclassing CNonDerivable();

void ConstructL(); private:

... // Omitted for clarity – private member data goes here };

So, to recap, unless you can guarantee that all objects have been constructed within your component on the heap, and are not derivable,

2 Class CBase also declares, but does not define, a private copy constructor (and corresponding assignment operator) to prevent the compiler generating the implicit versions that perform potentially unsafe shallow copies of member data. Neither the copy constructor nor assignment operator are implemented, in order to prevent copy operations of CBase classes – although of course you can implement your own, public, versions for derived classes where this is valid.

PREVENTING COMPATIBILITY BREAKS

283

you should not change the size of the member data of a class. If you follow these rules, you must also take into account that the object may be destroyed by an external component, which must deallocate all the memory it occupies. To guarantee that the object and the memory allocated for it are destroyed correctly, the class must have a virtual destructor. Again, you’ll inherit a virtual destructor from CBase when implementing a C class.

You are unlikely to have a valid reason for preventing a stack-based T class from being instantiated on the stack, so you should never modify the size of an externally visible T class.

Do Not Remove Anything Accessible

If you remove something from an API which is used by an external component, that component’s code will no longer compile against the API (a break in source compatibility) nor run against its implementation (a break in binary compatibility).

Thus, at an API level, you should not remove any externally-visible class, function, enumeration (or any value within an enumeration) or global data, such as string literals or constants.

At a class level, you should not remove any methods or class member data. Even private member data should not be removed, in general, because the size of the resulting object will change, which should be avoided for reasons described above.

Do Not Rearrange Accessible Class Member Data

If you don’t remove class member data but merely rearrange it, you can still cause problems to client code that accesses that data directly because, as I described above, the offset of the member data from the object’s this pointer will be changed.

You must not change the position of member data in a class if that data is:

public (or protected, if the client can derive from the class)

exposed through public or protected inline methods (which will have been compiled into client code).

This rule also means that you cannot change the order of the base classes from which a class multiply inherits without breaking compatibility, because this order affects the overall data layout of the derived object.

Do Not Rearrange the Ordinal of Exported Functions

In Chapter 13, I briefly mentioned the mechanism by which the API of a component is accessible to an external component, via a numerically

284

COMPATIBILITY

ordered list of exported functions which are ”frozen” into a module definition (.def) file. Each exported function is associated with an ordinal number, which is used by the linker to identify the function.

If you later re-order the .def file, say by adding a new export within the list of current exports, the ordinal number values will change and previously compiled code will be unable to locate the correct function. For example, say component A exports a method DoSomethingL() at ordinal 9, which is used by component B. Component A is later extended to export a new function, which is simply added to the start of the ordinal list, at ordinal 1. This changes the ordinal value of DoSomethingL() to 10. Unless component B is re-linked, it will still expect to be able to call DoSomethingL() at ordinal 9 but instead will call a completely different function. The change breaks binary compatibility. To avoid this, the new export should be added to the end of the .def file, which assigns it a new, previously unused, ordinal value.

The use of a .def file means that exported functions within a C++ class definition may be reordered safely, say to group them alphabetically or logically, as long as the underlying order of those exports in the .def file is not affected.

Do Not Add, Remove or Modify the Virtual Functions of Externally Derivable Classes

If a class is externally derivable (i.e. if it has an exported or inlined, public or protected constructor) you must not alter the nature of the virtual functions in any way, because this will break compatibility with calling code.

The reason why virtual functions should not be added to or removed from a derivable class is as follows: if the class is externally derivable and a derived class defines its own virtual functions, these will be placed in the virtual function table (vtable) directly after those defined by the base class, i.e. your class. If you add or remove a virtual function in the base class, you will change the vtable position of any virtual functions defined by a derived class. Any code that was compiled against the original version of the derived class will now be using an incorrect vtable layout.

This rule also applies to any base classes from which the class derives and the order from which they are derived if the class uses multiple inheritance. Changes to either will affect the layout of the vtable.

Likewise, you must not modify a virtual function if this means changing the parameters, the return type or the use of const. However, you can make changes to the internal operation of the function, for example a bug fix, as long as the documented behavior of the function is not changed.

PREVENTING COMPATIBILITY BREAKS

285

Do Not Re-Order Virtual Functions

Although not stated in the C++ standard, the order in which virtual member functions are specified in the class definition can be assumed to be the only factor which affects the order in which they appear in the virtual function table. You should not change this order, since client code compiled against an earlier version of the virtual function table will call what has become a completely different virtual function.

Do Not Override a Virtual Function that was Previously Inherited

If you override a virtual function which was previously inherited, you are altering the virtual function table of the class. However, existing client code is compiled against the original vtable and will thus continue to access the inherited base-class function rather than the new, overridden, version. This leads to inconsistency between callers compiled against the original version of the library and those compiled against the new version. Although it does not strictly result in incompatibility, this is best avoided.

For example, a client of CSiamese version 1.0 calling SleepL() invokes CCat::SleepL(), while clients of version 2.0 invoke

CSiamese::SleepL():

class CCat : public CBase // Abstract base class

{

public:

IMPORT_C virtual CCat() =0; public:

IMPORT_C virtual void PlayL(); // Default implementation

IMPORT_C virtual void SleepL(); // Default implementation

protected:

CCat();

};

class CSiamese : public CCat // Version 1.0

{

public:

IMPORT_C virtual CSiamese(); public:

//Overrides PlayL() but defaults to CCat::SleepL() IMPORT_C virtual void PlayL();

//...

};

class CSiamese : public CCat // Version 2.0

{

public:

IMPORT_C virtual CSiamese(); public:

//Now overrides PlayL() and SleepL() IMPORT_C virtual void PlayL(); IMPORT_C virtual void SleepL();

//...

};

286

COMPATIBILITY

Do Not Modify the Documented Semantics of an API

If you change the documented behavior of a class or global function, or the meaning of a constant, you may break compatibility with previously published versions used by client code, regardless of whether source and binary compatibility are maintained. As a very simple example, you may supply a class which, when supplied with a data set, returns the average value of that data. If the first release of the Average() function returns the arithmetic mean value, the second release of Average() should not be changed to return the median value, or some other interpretation of an average. As always, of course, if all the callers of the function can accept the change, or if the effect is limited to the internals of your own components, then such a modification is acceptable.

Do Not Remove const

The semantics of ”const” should not be removed, since this will be a source-incompatible change. This means that you should not remove the const-ness of a parameter, return type or class method that were originally specified as const.

Do Not Change from Pass by Value to Pass by Reference, or Vice versa

If parameters or return values were originally passed by value, you will break binary compatibility if you change them to reference values (or vice versa). When a parameter is passed by value to a function, the compiler generates a stack copy of the entire parameter and passes it to the function. However, if the function signature is changed to accept the parameter by reference, a word-sized reference to the original object is passed to the function instead. The stack frame for a pass-by-reference function call is thus significantly different from that for a pass-by-value function call. In addition, the semantics of passing by value and by reference are very different – as discussed in Chapter 20 – which inevitably causes binary incompatibility.

class TColor

{

...

private:

TInt iRed; TInt iGreen; TInt iBlue; };

//version 1.0

//Pass in TColor by value (12 bytes) IMPORT_C void Fill(TColor aBackground);

// version 2.0 – binary compatibility is broken