Fundamentals of Symbian C++/Descriptors
In Symbian C++, string handling is done using a set of classes known as descriptors. (They are called descriptors because they are 'self-describing:' a descriptor holds the length of the string or data it represents as well as type information to identify the underlying memory layout of the data.) Descriptors protect against buffer overrun and don't rely on NUL ('\0') terminators to determine the length of the string – as a consequence, they are also used to handle binary data.
This section provides a high-level introduction to the descriptor classes. Further information about how to use the classes can be found in the Descriptors Cookbook.
The character width of descriptor classes can be identified from their names. If the class name ends in 8 (for example, TPtr8) it has narrow (8-bit) characters, while a descriptor class name ending with 16 (for example, TPtr16) manipulates 16-bit character strings.
There is also a set of neutral classes that have no number in their name (for example, TPtr). The neutral classes are typedef-ed to the character width set by the platform, and are equivalent to their wide variants (the neutral descriptor classes were defined for source-compatibility purposes to ease a switch between narrow and wide builds, although today the Symbian platform is always built with wide characters). It is good practice to use the neutral descriptor classes where the character width does not need to be stated explicitly. However, to work with binary data, the 8-bit descriptor classes should be used explicitly, and some Symbian APIs take an 8-bit descriptor – for example, those to read from and write to a file – so file handling is independent of whether text or binary data is used.
The Descriptor Classes
The figure below shows the descriptor classes. As shown, the descriptor classes inherit from the base class TDesC, and they may be classified according to how their data is stored and accessed:
- Stack descriptors store their data as part of themselves.
- Pointer descriptors point to data stored elsewhere.
- Heap descriptors store their data in the heap.
In addition there is a related type known as a literal (you will frequently see literals referred to as literal descriptors and, because they are stored in the program binary code for an executable, they are also sometimes referred to as program binary descriptors). Literals aren't actually descriptors because they don't inherit from the base descriptor class TDesC; however they are closely related and have an operator()() method for converting them to a descriptor.
There are four 'abstract' classes: TDesC, TDes, TBufBase and TBufCBase. These are not abstract in the typical C++ sense because descriptors do not have pure virtual methods; however, they are abstract in the sense that they are not intended to be instantiated.
TBufCBase and TBufBase are an implementation convenience and will not be discussed further. There are six concrete descriptor types: TBuf, TPtr, RBuf, TPtrC, TBufC and HBufC, which, along with the literal type, can be instantiated and used directly in your programs to store or point to your string or binary data.
The following table summarizes the descriptors along with the approximate standard C equivalent:
|Type||Name||Modifiable||Approximate C equivalent||Notes|
|Literal||TLitC||No|| static const char 
|Built into ROM.|
|TDesC||No||n/a||Base class for all descriptors (except literals).|
|TDes||Yes||n/a||Base class for all modifiable descriptors.|
|TBuf||Yes||char ||Implemented as a thin template – the length of the descriptor is therefore fixed at compile time.|
|TBufC||Indirectly||const char |
(doesn't own the data pointed to)
|The data is stored separately from the descriptor object. The data pointed to can be anywhere that is addressable by your code.|
|TPtrC||No|| const char*
(doesn't own the data pointed to)
(owns the data pointed to)
|Used for dynamic data storage. Its use is preferred to HBufC. (The RBuf class was added to Symbian C++ several years after HBufC, which was already in widespread use throughout the platform. However, RBufs are safer and more convenient to work with.)|
|HBufC||Indirectly|| const char*
(owns the data pointed to)
|Used for dynamic data storage.|
Descriptor Base Classes: TDesC
In naming the descriptor classes, the T prefix indicates a simple type class (in accordance with the naming conventions in Fundamentals of Symbian C++/Types & Declarations; while the C suffix indicates that the class defines a non-modifiable type of descriptor, one whose contents are constant.
This class defines two property values: one for storing the type of the descriptor memory layout (stored in half a byte) and one for storing the length of the descriptor (stored in three-and-a-half bytes). TDes inherits from TDesC and additionally stores the maximum length of the data that is stored or pointed to by the descriptor.
The type field is not used to store the actual type of the descriptor, instead it is used to store the type of memory layout of the descriptor, determining how and where the descriptor data is stored and whether the maximum length is stored.
TDesC implements all the standard operations required on a constant string object, such as data access, matching and searching. The derived classes all inherit these methods and all constant descriptor manipulation is implemented by TDesC regardless of the derived type of the descriptor used.
As the ultimate base class, TDesC defines the fundamental layout of every descriptor type. The type field is used to store the type of memory layout of the descriptor – this allows concrete descriptors to be passed as TDesC or TDes parameters without the overhead of using virtual methods because the TDesC::Ptr() method uses a switch statement to identify the type of descriptor and thus know where the data is located. This requires that TDesC::Ptr() has hard-coded knowledge of the memory layout of all its subclasses, which consequently means that you can't create your own descriptor class deriving from TDesC.
This technique is used to avoid the need for each descriptor subclass to implement its own data access method using virtual function overriding. Doing so would add an extra four bytes to each derived descriptor object, for a virtual pointer (vptr) to access the virtual function table. Descriptors were designed to be as efficient as possible, and the size overhead to accommodate a C++ vptr was considered undesirable.
The length field is used to store the length of the data contained or pointed to by the descriptor. This removes the need for the descriptor data to be deliminated with a '\0' character because it is in C, a consequence of which is that it allows binary data to be stored as well as string data. It also means data beyond the bounds of the descriptor data cannot be accessed by mistake.
Descriptor Base Classes: TDes
The modifiable descriptor types all derive from the base class TDes, which is itself a subclass of TDesC. TDes has an additional four byte member variable to store the maximum length of data allowed for the current memory allocated to the descriptor. The MaxLength() method of TDes returns this value. Like the Length() method of TDesC, it is not overridden by derived classes. The contents of the descriptor can shrink and expand up to this value.
TDes defines a range of methods to manipulate modifiable string data, including those to append, fill and format the descriptor. All the manipulation code for descriptor modification is implemented by TDes and inherited by its derived classes.
The Derived Descriptor Classes
As described, the descriptor base classes TDesC and TDes implement all the generic descriptor manipulation code. The derived descriptor classes merely add their own construction and assignment code. However, it is the derived descriptor types that are actually instantiated and used. Although the base classes have member variables to store the length (and maximum length, for TDes) of the descriptor, they do not have storage for descriptor data and thus should never be instantiated. However, TDes and TDesC are used extensively as references in the arguments to functions.
Pointer Descriptors: TPtrC and TPtr
The data of a pointer descriptor is separate from the descriptor object itself and is stored elsewhere, for example, in ROM, on the heap or on the stack. The memory that holds the data is not 'owned' by the pointer descriptor.
A TPtrC object can be constructed from other descriptors, a pointer into memory, or a zero-terminated C string.
TPtrC is the equivalent of using const char* when handling strings in C. The data can be accessed but not modified. All the non-modifying operations defined in the TDesC base class are accessible to objects of type TPtrC, but none of the modification methods of TDes.
In comparison, the TPtr class can be used for access to, in addition to the modification of, a character string or binary data. All the modifiable and non-modifiable base-class operations of TDes and TDesC, respectively, are accessible to a TPtr. The following figure compares the memory layouts of TPtr and TPtrC.
The class defines constructors to allow objects of type TPtr to be constructed from a pointer into an address in memory, setting the length and maximum length as appropriate. The compiler also generates implicit default and copy constructors and a TPtr object may be copy-constructed from another modifiable pointer descriptor, for example, by calling the Des() method on a non-modifiable buffer.
_LIT(KLiteralDes1, "Jackdaws love my big sphinx of quartz");
// This is a literal which is described later
TBufC<60> buf(KLiteralDes1); // TBufC is described later
TPtr ptr(buf.Des()); // Copy construction – can modify the data in buf
TInt length = ptr.Length(); // Length=37 characters
TInt maxLength = ptr.MaxLength(); // Max length=60 chars, as for buf
TUint8* memoryLocation; // Valid pointer into memory
TInt len = 12; // Length of data to be represented
TInt maxLen = 32; // Maximum length to be represented
// Construct a pointer descriptor from a pointer into memory.
TPtr8 memPtr(memoryLocation, maxLen); // length=0, max=32
TPtr8 memPtr2(memoryLocation, len, maxLen); // length=12, max=32
Stack-Based Buffer Descriptors TBufC and TBuf
The stack-based buffer descriptors may be modifiable or non-modifiable. The string data forms:
_LIT(KDes, "Hello World!");
// This is a literal descriptor that is described later.
TPtrC pPtr(KDes); // Constructed from a literal descriptor.
TPtrC copyPtr(pPtr); // Copy constructed from another TPtrC.
TBufC<100> constBuffer(KDes); // Constant buffer descriptor.
TPtrC ptr(constBuffer); // Constructed from a TBufC.
// TText8 is a single (8-bit) character, equivalent to unsigned char.
const TText8* cString = (TText8*)"Hello World!";
// Constructed from a zero-terminated C string.
TUint8* memoryLocation; // Pointer into memory initialized elsewhere.
TInt length; // Length of memory to be represented.
memoryLocation = … // set to a memory location
Part of the descriptor object, as shown in the next figure, which compares the memory layouts of TBuf and TBufC. As their name suggests, they are typically declared on the stack, though they can also be allocated on the heap directly, or more commonly as class data members of CBase derived classes.
TBufC<n> is the non-modifiable buffer class, used to hold constant string or binary data. The class derives from TBufCBase (which derives from TDesC, and exists only as an inheritance convenience). TBufC<n> is a thin template class that uses an integer value to fix the size of the data area for the buffer descriptor object at compile time.
TBufC defines several constructors that allow non-modifiable buffers to be constructed from a copy of any other descriptor, or from a zero-terminated string. They can also be created empty and filled later, because, although the data is non-modifiable, the entire contents of the buffer may be replaced by calling the assignment operator defined by the class. The replacement data may be another non-modifiable descriptor or a zero-terminated string, but in each case the new data length must not exceed the length specified in the template parameter when the buffer was created.
_LIT(KPalindrome, "Satan, oscillate my metallic sonatas");
TBufC<50> buf1(KPalindrome); // Constructed from literal descriptor
TBufC<50> buf2(buf1); // Constructed from buf1
// Constructed from a NULL-terminated C string
TBufC<30> buf3((TText16*)"Never odd or even");
TBufC<50> buf4; // Constructed empty, length = 0
// Copy and replace.
buf4 = buf1; // buf4 contains data copied from buf1, length modified
buf1 = buf3; // buf1 contains data copied from buf3, length modified.
buf3 = buf2; // Panic! Max length of buf3 is insufficient for buf2 data
TBuf<n> is used when a modifiable buffer is required. It derives from TBufBase, which itself derives from TDes, and inherits the full range of descriptor operations in TDes and TDesC. The class defines a number of constructors and assignment operators, similar to those offered by its non-modifiable counterpart, TBufC<n>.
Symbian C++ defines a number of classes and typedefs, which are descriptors or contain descriptors that could be used inefficiently if used on the stack. It's advisable therefore to be aware of the amount of space the following consume and to be wary of using them (or your own large TBuf/TBufC descriptors) on the stack:
Dynamic Descriptors: HBufC and RBuf
The heap descriptors are HBufC and RBuf and their data is stored on the heap. They are responsible for the allocation and de-allocation of this memory.
The heap descriptors would be used in similar situations to where a malloc()-ed cell would be used in C.
RBufs are not named HBuf, because while HBufC objects are directly allocated on the heap (where H stands for heap, HBufC is allocated on the heap though is not called CBufC because it doesn't inherit from CBase. It is the only class in Symbian C++ that begins with the prefix H), RBuf objects are not – the R prefix indicates that the class owns and manages its own memory resources.
These types are useful when the amount of data to be held by the descriptor cannot be determined until run time or a large amount of data is required to be used as a local variable (the stack size is limited in the Symbian platform and you should always strive to minimize its usage).
With both RBufs and HBufCs, if the heap size needs to expand this is not done automatically and it's up to you to dynamically resize the descriptor.
With both HBufCs and RBufs you are responsible for memory management, so always be aware of cleanup issues to ensure that there is no risk of memory leaks. HBufCs are placed on the cleanup stack using an overload of CleanupStack::PushL(), while RBufs have their own method for pushing to the cleanupstack: RBuf::CleanupClosePushL().
The HBufC8 and HBufC16 classes (and the neutral version HBufC, which is typedefed to HBufC16) provide a number of static NewL() functions to create the descriptor on the heap. These methods may leave if there is insufficient memory available. All heap buffers must be constructed using one of these methods or from one of the Alloc() or AllocL() methods of the TDesC class that spawn an HBufC copy of any existing descriptor. Once the descriptor has been created to the size required, it is not automatically resized if more space is required. Additional memory must be reallocated using the ReAlloc() or ReAllocL() methods. The memory layout of an HBufC* descriptor is shown in the next figure.
As the C suffix of the class name, and the inheritance hierarchy indicates, HBufC descriptors are not directly modifiable, although the class provides assignment operators to allow the entire contents of the buffer to be replaced. To modify an HBufC object at runtime a modifiable pointer descriptor, TPtr, must first be created using the HBufC::Des() method.
_LIT(KPalindrome, "Do geese see God?");
// Allocate an empty heap descriptor of max length 20
HBufC* heapBuf = HBufC::NewLC(20);
// Modify the heap descriptor through a TPtr
ptr = stackBuf; // Copies stackBuf contents into heapBuf
// Allocate a heap descriptor containing stackBuf
HBufC* heapBuf2 = stackBuf.AllocLC();
*heapBuf2 = KPalindrome2; // Copy and replace data in heapBuf2
Class RBuf is derived from TDes, so an RBuf object can be modified without the need to create a TPtr around the data first, which often makes it preferable to HBufC. On instantiation, an RBuf object can allocate its own buffer or take ownership of pre-allocated memory or a pre-existing heap descriptor.
RBuf descriptors are typically created on the stack, and hold a pointer to a resource on the heap for which it is responsible for cleanup.
RBuf objects can be instantiated using the Create(), CreateMax() or CreateL() methods to specify the maximum length of descriptor data that can be stored. It is also possible to instantiate an RBuf and copy the contents of another descriptor into it, as follows:
_LIT(KHelloRBuf, "Hello RBuf!"); // Literal descriptor.
CreateL() allocates a buffer for the RBuf to reference. If that RBuf previously owned a buffer, CreateL() will not clean it up before assigning the new buffer reference, so this must be done first by calling Close() to free any pre-existing owned memory.
Alternatively, an RBuf can be instantiated and take ownership of a pre-existing section of memory using the Assign() method.
// Taking ownership of HBufC.
HBufC* myHBufC = HBufC::NewL(20);
Assign() will also orphan any data already owned by the RBuf, so Close() should be called before re-assignment, to avoid memory leaks.
The RBuf class doesn't manage the size of the buffer and re-allocate it if more memory is required for a particular operation. If a modification method, such as Append(), is called on an RBuf object for which there is insufficient memory available, a panic will occur. As a programmer, you are responsible for re-allocating memory to the descriptor if it is required, using the ReAllocL() method:
// myRBuf is the buffer to be resized, for example, for an Append() operation.
myRBuf.CleanupClosePushL(); // Push on to cleanup stack for leave-safety
myRBuf.ReAllocL(newLength); // Extend to newLength
CleanupStack::Pop(); // Remove from cleanup stack
It is easy to migrate code that previously used HBufC to use RBuf, which can be desirable when a dynamic buffer is modifiable. HBufC is rather clumsy to modify, since a TPtr object must first be constructed by calling Des() on it. The following example illustrates this:
// Defined elsewhere
static void FileReader::ReadL(TDes& aModifiable);
TInt KMaxNameLength = 64;
HBufC* socketName = HBufC::NewL(KMaxNameLength);
// Create writable TPtr in order to modify socketName.
This can be converted to the following:
The first sample requires construction of a separate TPtr around the HBufC, so is slightly less efficient. Because the code using RBuf is simpler, it is also easier to understand and maintain.
A program binary consists of the compiled and linked together DLLs and EXEs that make up a program. A literal string is one that is included in the program binary. As a program binary is executed either directly from ROM or from read-only RAM, any literals it contains are also read-only.
In standard C, a string literal would be included in the program binary by declaring the variable as a static constant array and initializing it with character data:
static const char KTxtHelloWorld = “Hello World!”;
In Symbian C++, literal strings are included in the program binary using the TLitC class. However, you never actually use this class directly. Instead, you use it via the macro _LIT, as follows:
This adds the wide string literal "Hello World!" of type TLitC to the program binary and associates the symbol KTxtHelloWorld with it so that it can be subsequently referenced.
The binary layout of TLitC is designed to be identical to a TBufC which allows a TLitC to be treated as a descriptor.
As with the descriptor classes, there are wide, narrow and neutral variants of literals. _LIT8 is useful, for example, in HTTP/socket programming for storing ASCII commands.
Descriptor Class Types: Decision Table
The figure below summarizes how the knowledge in this chapter can be applied when deciding what type of descriptor class to use.
Descriptors as Function Parameters and Return Types
The following guidelines should be followed when using descriptors as method parameters or method return types:
- To pass a descriptor as an input parameter into a method, that is to pass descriptor data that you will read from within the method but will not attempt to change, use const TDesC& as the parameter type.
- To pass an in-out parameter to a method, that is to pass descriptor data that you want to change within your method, use TDes& as the parameter type.
- If you are returning an entire descriptor, which will still exist while the caller of the method uses it, and the descriptor is not to be modified, then you should return const TDesC&.
- If you are returning a part of a descriptor, which will still exist while the caller of the method uses it, and the descriptor is not to be modified, then you should return TPtrC.
- If you are returning an entire descriptor, which will still exist while the caller of the method uses it, and the descriptor is to be modified, then you should return TDes&.
- If you are returning a part of a descriptor, which will still exist while the caller of the method uses it, and the descriptor is to be modified, then you should return TPtr&.
- If the data is to be constructed within a method and ownership passed to the caller there are three options:
- If the data has a known maximum length which isn't too big (< 256) then return a TBufC.
- If the data is large then create and return a HBufC* or
- Pass an RBuf as a parameter as follows (an RBuf cannot be returned from a function because its copy constructor is protected):
void copyTextL(RBuf& aDes)
if (aDes.MaxLength() < KTxtHello().Length())
aDes = KTxtHello;
Note that the reason for mandating that descriptors should be reference parameters (TDes& or const TDesC&) and not values (TDes or const TDesC) is not just one of efficiency. Passing by value uses static binding, and passes the base class in question (TDesC and TDes), which contain no string data, and thus should never be used directly.
void BadExample(const TDesC aString) // Should be const TDesC&
TBufC<10> buffer(aString); // buffer will contain random values