×
Namespaces

Variants
Actions

Fundamentals of Symbian C++/Leaves & The Cleanup Stack/ru

From Nokia Developer Wiki
Jump to: navigation, search
Article Metadata

Статья
Перевод:
hamishwillee
Последнее редактирование: hamishwillee (09 Dec 2011)

Перевод еще не закончен!

Одни из самых фундаментальных идиом в Symbian C++ - это облегченный вариант обработки исключений, или так называемые сбросы, и использование стека очистки для управления удалениями объектов и очистки ресурсов при возникновении исключений (например, неудачное выделение памяти для объекта из-за нехватки места). Сбрасывающую функцию можно описать как ту, которая, при обнаружении ошибки, тут же останавливается и "разматывает" стек вызовов, осуществляя очистку памяти, до того, как обнаружит обработчик этого сброса. Это отличает ее от функции, которая возвращает ошибку в вызвавшую ее функцию.

Эта статья объясняет механизм обработки исключений, как очищается память, и как писать код, избегая утечек памяти при сбросах.

Contents

Вступление

В приложениях для настольных компьютеров последствия утечек памяти не столь значительны, так как памяти предостаточно и любая утечка памяти восстанавливается, как только приложение закроется или перезагрузится система. Важно помнить, что платформа Symbian разрабатывалась для устройств с ограниченной памятью, и эти устройства обычно не перезагружаются в течение нескольких дней, недель и даже месяцев. Приложения часто не закрываются, а оставляются запущенными на неопределенный срок. Правильное использование стека очистки устранит утечки памяти или других ресурсов, которые могут появиться вслед обнаруженному исключению. (Смотри также отдельную статью, которая объясняет как устранять утечки памяти в коде Symbian C++ (en))

Symbian C++ проектировался до того, как стандарт C++ был формализирован и до того, как компиляторы стали полностью поддерживать обработку исключений с помощью try, catch или throw. Вдобавок, такая обработка исключений раздувает размер скомпилированного кода и потребляет много памяти, поэтому стандартные C++ исключения не использовались.

Сейчас платформа Symbian поддерживает стандартные C++ исключения, что упрощает портирование существующего C++ кода на платформу Symbian. Смотри Сравнение Сбросов и Исключений (en). Однако, сбросы все еще являются фундаментальной частью обработки исключений в Symbian и используются во всей системе. Так что даже если ваш код использует только стандартные C++ исключения, важно знать, как смешивать сбросы и исключения для корректной работы гибридного кода.

Исключения реализованы с учетом стандартных C++ исключений и при должном внимании смешивать сбросы и исключения возможно. Больше деталей можно найти здесь: Exception Handling.

Сбросы эквивалентны команде throw в стандартной C++ обработке исключений. Как и C++ исключения, сбросы используют для передачи ошибок туда, где они могут быть обработаны. Не следует их использовать для направления нормального течения программной логики. На платформе Symbian сбросы не останавливают исполнительный поток (в отличие от паник).

Причины Сбросов

Сброс может возникнуть в функции, если она:

  • явно вызывает одну из системных функций в классе User, которые вызывают сброс, таких как User::Leave() или User::LeaveIfError()
  • использует перегруженную в Symbian C++ форму оператора new, который принимает ELeave в качестве параметра, и памяти оказалось не достаточно для размещения нового объекта
  • вызывает другую функцию, которая может сбросить (по любой из вышеперечисленных причин) и не использует макрос TRAP.

Теперь давайте изучим по очереди каждый из этих путей, приводящих к сбросу.

Функции Класса User, Вызывающие Сброс

Класс User - это полезный статический класс, определенный в e32std.h. Этот класс предоставляет комплекс внешних системных функций, среди которых есть такие, которые вызывают сброс в текущем потоке. Эти функции определены так:

IMPORT_C static void Leave(TInt aReason);
IMPORT_C static void LeaveNoMemory();
IMPORT_C static TInt LeaveIfError(TInt aReason);
IMPORT_C static TAny* LeaveIfNull(TAny* aPtr);

Различные методы:

  • User::Leave() сбрасывает, используя целое число как код сброса.
  • User::LeaveIfError() проверяет принимаемое целое число и вызывает User::Leave() (передавая это число как код сброса), если оно меньше нуля. (Коды ошибок вида KErrXXXX, определенные в e32err.h имеют отрицательные значения, например, KErrNoMemory равно -4.) Функция{{icode|User::LeaveIfError()} полезна для превращения функций, возвращающих коды ошибок (одну из констант вида KErrXXXX, объявленных в e32err.h)) в функцию, которая сбрасывает с тем же значением. Считается плохой практикой создавать функции, которые одновременно сбрасывают и возвращают коды ошибок.
  • User::LeaveNoMemory() просто сбрасывает с кодом KErrNoMemory; эта функция эквивалентна вызову User::Leave(KErrNoMemory).
  • User::LeaveIfNull() принимает указатель и сбрасывает с кодом KErrNoMemory? если он равен NULL.

Размещение Объекта на Куче, Используя new(ELeave)

Symbian C++ перегружает глобальный оператор new для сброса, если на куче недостаточно памяти для успешного размещения объекта. Применение этой перегрузки позволяет использовать указатель на размещенный объект без дальнейшей проверки на успешность размещения, потому что оператор сбросит, если размещение не удастся. Например:

CBook* InitializeBookL()      // Буква ‘L’ - это часть конвенции о именах
// в Symbian С++, и она указывает на то,
// что функция может сбросить.
{
CBook* book = new(ELeave) CBook();
book->Initialize(); // Проверять book на равенство с NULL не требуется.
return (book);
}

Код выше предпочтительней следующему:

CBook* InitializeBookL()
{
CBook* book = new CBook();
User::LeaveIfNull(book);
book->Initialize();
return (book);
}

Или следующему:

CBook* InitializeBook()
{
CBook* book = new CBook();
if (book)
{
book->Initialize();
return (book);
}
else
return (NULL);
}

Сбросы предпочтительней примеру выше, так как они упрощают не только метод InitializeBook(), но и код, который этот метод вызывает.

Код, Который Может Сбросить

Как узнать, что функция может сбросить? Как уже кратко упоминалось в предыдущей секции, существует правило именования функций для указания на возможность сброса функцией - больше похожее на throw(...) в C++.

Если функция может сбросить, то в конце ее имени должна стоять буква L для распознавания ее как таковой.

Во всей конвенции о именах в Symbian C++ это, пожалуй, самое важное правило: если сбрасывающая функция не названа корректно, вызывающая ее функция не узнает, что нужно принять меры для обработки потенциального исключения. Если произойдет сброс, и он не выловится (макросом TRAP), возникнет паника.

Так как это не часть стандарта C++, суффиксы L не могут быть проверены во время компиляции, и иногда о них можно забыть. The Symbian Foundation предоставляет полезный инструмент, LeaveScan для проверки кода на наличие некорректно названных сбрасывающих функций. Другие инструменты сканирования исходного кода также можно использовать для этой цели - они доступны на сайте: Category:Tools.

Сбрасывающие функции не должны возвращать код ошибки. Любая ошибка, возникшая в сбрасывающей функции должна передаваться через код сброса; если функция не сбрасывает, то считается, что она завершилась успешно. Как правило, сбрасывающие функции должны возвращать void, если они не возвращают указатель или ссылку на ресурс, который они разместили.

Макросы TRAP и TRAPD

Макросы TRAP и TRAPD - это 'упряжки', которые ловят сбросы и позволяют их обработать:

TRAPD(result, MayLeaveL());

Если в MayLeaveL() произойдет сброс, стек будет невредим, память очистится стеком очистки, а управление передастся сразу следом за макросом TRAP. Переменная result будет содержать код ошибки связанной со сбросом (или KErrNone, если сброса не было).

Любая функция, вызванная из MayLeaveL(), исполняется внутри контекста макроса TRAP. Любой сброс, возникший во время исполнения MayLeaveL(), отловится независимо от того, как глубоко в стеке вызова он произошел.

Поэтому TRAP/TRAPD примерно тоже самое, что и catch {} в стандартной обработке C++ исключений.

Макросы TRAP иTRAPD отличаются всего лишь тем, что TRAPD объявляет переменную, в которую возвращается код ошибки, в то время как код, использующий TRAP, должен перед этим самостоятельно объявить переменную типа TInt. Следующие фрагменты кода эквивалентны:

TRAPD(result, MayLeaveL());
if (KErrNone!=result)
{
// Обработка ошибок.
}
 
TInt result;
TRAP(result, MayLeaveL());
 
if (KErrNone!=result)
{
// Обработка ошибок.
}

Макрос TRAP также можно вставлять для перехвата и обработки сбросов на разные уровни, где с ними можно было бы лучше справиться. Однако, каждый TRAP влияет на размер бинарника и скорость выполнения, поэтому количество TRAP нужно максимально уменьшать. Например, предположим, что следующей функции нужно вызвать несколько сбрасывающих функций, но она не должна сбрасывать сама. Казалось бы, можно поставить каждый вызов в макрос TRAP:

TInt NonLeavingFunction()
{
TRAPD(result, MayLeaveL());
if (KErrNone==result)
TRAP(result, MayAlsoLeaveL());
if (KErrNone==result)
TRAP(result, MayBeALeaverL());
 
// Возвращает ошибку или KErrNone при успешном исполнении.
return (result);
}

Следующий вариант предпочтительней, как более эффективный:

TInt NonLeavingFunction()
{
TRAPD(result, LeavingFunctionL());
 
// Возвращает ошибку или KErrNone при успешном исполнении.
return (result);
}
 
void LeavingFunctionL()
{
MayLeaveL();
MayAlsoLeaveL();
MayBeALeaverL();
}

Что вызывает утечку памяти?

Если объект распределен на куче и на него ссылается только переменная в стеке, тогда произойдет утечка памяти при возникновении сброса.

Это происходит, потому что, при создании локального указателя на объект, расположенного на куче, указатель находится в стеке. При возникновении сброса, стек восстанавливается из копии в вид на тот момент времени, когда макрос TRAP был вызван. Объект на куче осиротел - он все еще существует на куче, но указатель в стеке был сброшен. Так как память невосстановима, это - утечка памяти.

Следующий код иллюстрирует это:

void UnsafeL()
{
 
// Размещение на куче.
CBook* book = new(ELeave) CBook();
 
book->InitializeL(); // Небезопасен - book потеряется, если
 
// InitializeL() сбросит
delete book;
}

Память, распределенная на куче для хранения объекта типа CBook, на которую указывает book, станет недоступной, если вызов InitializeL() сбросит. Для предотвращения этого следует использовать стек очистки, что описывается в следующей секции.

Заметьте, аргумент выше не применим для членов-переменных, потому что указатель на память содержится в родительском объекте, а не в стеке. Никогда не ставьте члены-переменные в стек очистки по причинам, описанным в следующей секции.

The Cleanup Stack

The previous section explained that a memory leak can occur if heap objects are accessible only through pointers local to the function that leaves. Let’s look at this in terms of the Symbian C++ types that were discussed here.

  • T class objects are leave safe – they generally do not contain any member data that requires explicit destruction; if they have a destructor defined, it gets called when the object goes out of scope (the nature of T class objects has changed as Symbian C++ has evolved, here and The Implications of Leaving in a Destructor for further details).
  • C class objects, which are always created on the heap, are not leave safe if created as a local variable but are leave safe if created as a member variable of another class (which in turn, by definition, must be another C class).
  • R class objects are similarly not leave safe if created as a local variable, since the resources they own must be freed in the event of a leave (through a call to the appropriate Close() or Release() function).
  • M class objects may or may not be leave safe, depending on the type of class the interface has been mixed in to. To better understand this point, see the MPolygon example later.

One way to make objects leave safe is to place a TRAP (or TRAPD) macro around every potential leaving call. However, the use of TRAPs should be limited for reasons of efficiency, and to avoid clumsy code that constantly traps leaves and checks leave codes. In effect, code that uses multiple trap harnesses to catch every possible leave is simply reverting to the basic error-handling approach that leaves were designed to avoid. Instead, Symbian C++ provides a mechanism for storing pointers that are not leave safe, to ensure they are cleaned up in the event of a leave. This is called the cleanup stack. Each thread has its own cleanup stack and, as long as pointers are stored on it before calling code that may leave, the cleanup stack destroys them automatically. The cleanup stack is rather like a Symbian version of the standard C++ library’s smart pointer, auto_ptr.

It isn’t necessary to create a cleanup stack for a GUI application thread, since the application framework creates one. However, a cleanup stack must be created if writing a server, a simple console-test application or when creating an additional thread that contains code that uses the cleanup stack. This is done as follows:

CTrapCleanup* theCleanupStack = CTrapCleanup::New();
 
... // Code that uses the cleanup stack within a TRAP macro.
 
delete theCleanupStack;

Once created, the cleanup stack is accessed through the static member functions of class CleanupStack, defined in e32base.h:

class CleanupStack
{
 
public:
 
IMPORT_C static void PushL(TAny* aPtr);
IMPORT_C static void PushL(CBase* aPtr);
IMPORT_C static void PushL(TCleanupItem anItem);
IMPORT_C static void Pop();
IMPORT_C static void Pop(TInt aCount);
IMPORT_C static void PopAndDestroy();
IMPORT_C static void PopAndDestroy(TInt aCount);
IMPORT_C static void Check(TAny* aExpectedItem);
 
inline static void Pop(TAny* aExpectedItem);
inline static void Pop(TInt aCount,TAny* aLastExpectedItem);
inline static void PopAndDestroy(TAny* aExpectedItem);
inline static void PopAndDestroy(TInt aCount,TAny* aLastExpectedItem);
 
};

Using the Cleanup Stack

The following code illustrates a leave-safe version of the function UnsafeL() discussed earlier:

void NowSafeL()
{
 
// Heap allocation.
CBook* book = new(ELeave) CBook();
 
// Push on to the cleanup stack.
 
CleanupStack::PushL(book);
book->InitializeL(); // Safe
 
// Pop from the cleanup stack.
 
CleanupStack::Pop(book);
 
delete book;
}

If InitializeL() leaves, the object pointed to by book is destroyed by the cleanup stack. If no leave occurs, the book pointer is popped from the cleanup stack and the object to which it points is deleted explicitly when finished with. This code could equally well be replaced by a call to CleanupStack::PopAndDestroy(book) that pops the pointer and makes a call to the destructor in one step.

In the example above, you may have noticed that PushL() is a leaving function, which at first sight appears to be self-defeating. If the cleanup stack is supposed to make pointers leave safe, how can it potentially cause a leave when you use it and still be helpful? The reason PushL() may leave is that it may need to allocate memory to store the pointers passed to it (and in low-memory conditions this could fail). However, the object passed to PushL()will not be orphaned if it leaves. This is because, when the cleanup stack is created, it has at least one spare slot. When PushL() is called, the pointer is added to the next vacant slot and then, if there are no remaining slots available, the cleanup stack attempts to allocate some for future pushes – it is this allocation that may fail. If there is a failure, the pointer has already been added to the stack so its object can be cleaned up.

The cleanup stack is expanded four slots at a time for efficiency, and Pop() and PopAndDestroy() do not release the memory for slots once they have been allocated. This memory is available for future pushes on to the cleanup stack until the cleanup stack is destroyed.

In a function, if a pointer to an object is pushed on to the cleanup stack and remains on it when that function returns, the naming convention is to append a C to the function name. For example:

CBook* NowSafeLC()
{
// Heap allocation.
CBook* book = new(ELeave) CBook();
 
// Push on to the cleanup stack.
CleanupStack::PushL(book);
 
book->InitializeL();
 
return (book);
}

This is used most frequently in static factory functions for C classes.

Ordering Calls to PushL() and Pop() or PopAndDestroy()

Pointers can be pushed on to and popped off the cleanup stack, but since it’s a stack, for any series of pushes, the corresponding pops must occur in reverse order. It’s a good idea to name the pointer as it is popped, so debug builds can check that it is the correct pointer and flag up any programming errors. For example:

void TestPhonesL()
{ // Each object is pushed on to the cleanup stack immediately
// it is allocated, in case the next allocation leaves.
 
CPhone* phoneA = CPhone::NewL(A);
CleanupStack::PushL(phoneA);
 
CPhone* phoneB = CPhone::NewL(B);
CleanupStack::PushL(phoneB);
 
CPhone* phoneC = CPhone::NewL(C);
CleanupStack::PushL(phoneC);
 
CPhone* phoneD = CPhone::NewL(D);
CleanupStack::PushL(phoneD);
 
... // Leaving functions called here.
 
// Various ways to remove the objects from the stack and delete them:
 
// (1) All with one anonymous call – OK
// CleanupStack::PopAndDestroy(4);
 
// (2) Each object individually to verify the code logic
// Note the reverse order of Pop() to PushL().
 
// This is long-winded.
// CleanupStack::PopAndDestroy(phoneD);
// CleanupStack::PopAndDestroy(phoneC);
// CleanupStack::PopAndDestroy(phoneB);
// CleanupStack::PopAndDestroy(phoneA);
 
// (3) All at once, naming the last object. This is the best solution.
CleanupStack::PopAndDestroy(4, phoneA);
 
}

Using the Cleanup Stack with T, R and M Classes

As you can see from the definition of class CleanupStack given earlier, there are three overloads of the PushL() method. These determine how the item is destroyed when it is cleaned up when a leave occurs or a call is made to CleanupStack::PopAndDestroy().

IMPORT_C static void PushL(CBase* aPtr);

This overload takes a pointer to a CBase-derived object. On cleanup it is destroyed by invoking delete, thus calling the virtual destructor of the CBase-derived object.

IMPORT_C static void PushL(TAny* aPtr);

If the object pushed on to the cleanup stack is a pointer to an object that does not derive from CBase, this overload is invoked. On cleanup, the memory referenced by the pointer is simply deallocated by invoking User::Free() (delete is not called on it, so no destructor is invoked).

This overload of PushL() is called, for example, when pointers to heap-based T class objects are pushed on to the cleanup stack. If the T class has no member data that requires explicit de-allocation this is fine because there is no requirement for cleanup beyond de-allocation of the heap memory occupied by the T class. However, if the T class has a destructor that requires calling this overload of PushL() will not suffice, instead a TCleanupItem should be used.

IMPORT_C static void PushL(TCleanupItem anItem);

This overload takes an object of type TCleanupItem, which is designed to accommodate objects requiring customized cleanup processing. The TCleanupItem object encapsulates a pointer to the object to be stored on the cleanup stack and a pointer to a function to clean up that object (a local function or a static method of a class). A leave or a call to PopAndDestroy() removes the object from the cleanup stack and calls the associated cleanup function.

Symbian C++ also provides a set of template utility functions, each of which generates an object of type TCleanupItem and pushes it on to the cleanup stack:

  • CleanupClosePushL() – the cleanup method calls Close() on the object in question. This utility method is usually used to make stack-based R class objects leave safe. For example:
void UseFilesystemL()
{
 
RFs theFs;
User::LeaveIfError(theFs.Connect());
 
CleanupClosePushL(theFs);
... // Call functions which may leave,
// theFs.Close() is called if a leave occurs.
 
CleanupStack::PopAndDestroy(&theFs); // Note the & is required
}
  • CleanupReleasePushL() – the cleanup method calls Release() on the object in question. This method is typically used to push an object referenced through an M class (interface) pointer that provides a Release() method.
  • CleanupDeletePushL() – the cleanup method calls delete on the pointer passed into the function. This is also typically used for passing M class pointers.

The following simplistic example illustrates the reason why this is needed (for those interested, a longer discussion can be found on our page about mixin inheritance and the cleanup stack.

A C class, CSquare, uses multiple inheritance from CBase and an M class interface, MPolygon. The object is instantiated and the M class interface pushed on to the cleanup stack. The object is later destroyed by a call to PopAndDestroy(). If the pushing was done with CleanupStack::PushL(), the PushL(TAny*) would be used because the pointer is not a CBase pointer. The PushL(TAny*) overload means that User::Free() is invoked on the pointer on cleanup, but this would result in a User 42 panic. That’s because User::Free() can't find the heap information it needs to release the memory to the system when passed an M class pointer to the sub-object of a C class. Even if it could, the virtual destructor of CSquare would not be called, so the best-case scenario would be a memory leak! The solution is to use CleanupDeleteL() to call delete on the M class pointer and invoke the virtual destructor correctly for the CSquare class.

class MPolygon
{
 
public:
 
virtual TInt CalculateNumberOfVerticesL() =0;
virtual ~MPolygon(){};
 
};
 
class CSquare : public CBase, public MPolygon
{
public:
static CSquare* NewL();
virtual TInt CalculateNumberOfVerticesL();
virtual ~CSquare(){};
 
private:
CSquare(){};
 
};
 
enum TPolygonType { ESquare };
 
static MPolygon* InstantiatePolygonL(TPolygonType aPolygon);
 
MPolygon* InstantiatePolygonL(TPolygonType aPolygon)
{
if (ESquare==aPolygon)
return (CSquare::NewL());
else
return (NULL);
}
 
/*static*/ CSquare* CSquare::NewL()
{ // No two-phase construct needed here.
CSquare* me = new(ELeave) CSquare();
return (me);
}
 
TInt CSquare::CalculateNumberOfVerticesL()
{ // Simple implementation, but others may leave.
return (4);
}
 
void DoStuffL()
{
MPolygon* square = InstantiatePolygonL(ESquare);
CleanupDeletePushL(square);
 
// ... Leaving calls go here.
ASSERT(4==square->CalculateNumberOfVerticesL());
CleanupStack::PopAndDestroy(square);
}
  • CleanupArrayDeletePushL() – this method is used to push a pointer to a heap-based C++ array of T class objects (or built-in types) on to the cleanup stack. When PopAndDestroy() is called, the memory allocated for the array is cleaned up using delete[]. No destructor is called on the elements of the array.

Member Variables and the Cleanup Stack

A class member variable (prefixed by i) should not be pushed on to the cleanup stack. The owning object destroys it when appropriate, so it does not need to be made leave safe through use of the cleanup stack.

Note that if you do add a member variable to the CleanupStack, there will be a panic in the event of a leave. This is because both the cleanup stack and the parent object will try and clean up the memory, resulting in a double deletion exception.

Mixing Leaves, Exceptions and the Cleanup Stack

Symbian C++ TRAPs and leaves are implemented internally in terms of C++ exceptions (a leave is an XLeaveException). A TRAP will panic if it catches any other kind of exception. This means that, if you are mixing C++ exceptions in with leaves and TRAPs, you must not throw an exception within a leaving function unless also you catch and handle it internally in that function.

A normal catch block will not manage the cleanup stack correctly either, so code that throws exceptions should not use the cleanup stack directly or indirectly. If you are calling leaving functions from exception-style code, do not use the cleanup stack, but make the calls as required and catch and handle any exceptions that arise.

Memory-Management Macros

Symbian OS provides a set of debug-only macros that can be added directly to code to check that memory is not leaked.

There are a number of macros available, but the most commonly used are defined as follows:

#define __UHEAP_MARK User::__DbgMarkStart(RHeap::EUser)
#define __UHEAP_MARKEND User::__DbgMarkEnd(RHeap::EUser,0)

The macros verify that the default user heap is consistent. The check starts after a call to __UHEAP_MARK and a later call to __UHEAP_MARKEND verifies that the heap is in the same state as at the start. If heap cells allocated in the interim have not been freed, a panic is raised to indicate a potential leak. The panic is ALLOC nnnnnnnn, where nnnnnnnn is a hexadecimal pointer to the heap cell in question.

The heap-checking macros can be nested used anywhere in code. They are not compiled into release builds so do not have any impact on the code size or speed of production code. Sometimes you may find that you get false positives where memory is allocated by the system and not freed within a MARK/MARKEND pair. A good example is the cleanup stack which is expanded to store pointer values where necessary, but when they are popped off, the allocated memory is retained and re-used later or destroyed when the cleanup stack is deleted.

In addition, an application must gracefully handle any out-of-memory leaves that occur when memory is not available for allocation. Symbian OS provides a set of macros to simulate heap failure, either on the next allocation, randomly or by allowing you to specify the number of allocations that can succeed before failure. These are extremely useful: for example, a test suite can call a function repeatedly, increasing the heap failure point by one, to check that each allocation fails gracefully. Please consult the Symbian Developer Library documentation for more information about __UHEAP_FAILNEXT and __UHEAP_SETFAIL for more information about how to use them.

See also


Licence icon cc-by-sa 3.0-88x31.png© 2010 Symbian Foundation Limited. This document is licensed under the Creative Commons Attribution-Share Alike 2.0 license. See http://creativecommons.org/licenses/by-sa/2.0/legalcode for the full terms of the license.
Note that this content was originally hosted on the Symbian Foundation developer wiki.

This page was last modified on 9 December 2011, at 02:58.
70 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×