In the previous post I introduced some of the common cases for avoiding memory leaks with judicious use of unique_ptr
. Of course, not all memory management situations are as straightforward, especially when dealing with low-level “C-style” APIs. Let’s explore a few more examples.
Release ownership to legacy API
To release ownership of an object to code that doesn’t understand unique_ptr
, you can use the release
method. This ensures that at least your usage of the object is exception-safe up to the point you relinquish it.
void LegacyCodeToTakeResponsibility(MyClass * p) { FuncTrace t(__FUNCTIONW__); wcout << L"I take responsibility for " << p->get_Name() << endl; // ... sometime later ... delete p; } void ReleaseOwnership() { FuncTrace t(__FUNCTIONW__); MyClassUPtr p = make_unique<MyClass>(L"to-be-released"); // ... do some work here that might cause an exception ... // Okay, now it's safe to give this away. LegacyCodeToTakeResponsibility(p.release()); if (!p) { wcout << L"I can't use this instance anymore." << endl; } } int wmain(int argc, wchar_t ** argv) { ReleaseOwnership(); wcout << L"Final count: " << Counter::N << endl; return 0; }
The output shows that the object is only released once and that the unique_ptr
no longer refers to any valid instance:
{ Enter ReleaseOwnership (Dynamic allocation) MyClass:to-be-released { Enter LegacyCodeToTakeResponsibility I take responsibility for to-be-released ~MyClass:to-be-released (Dynamic deallocation) } Leave LegacyCodeToTakeResponsibility I can't use this instance anymore. } Leave ReleaseOwnership Final count: 0
Use custom deleter
Not all objects can be freed using delete
. This is particularly true of resources that you did not explicitly allocate. In this example, a custom deleter is defined (as a template class which overloads the function call operator) which can free a buffer allocated by FormatMessage using LocalFree
as documented by the API:
#include <Windows.h> // ... template <typename T> struct LocalFreeDelete { void operator()(T * ptr) const { wcout << L"LocalFree" << endl; LocalFree(ptr); --Counter::N; } }; void FormatSomeMessage() { FuncTrace t(__FUNCTIONW__); wchar_t const * message = L"Name: %1 / City: %2"; wchar_t const * name = L"Xyz"; wchar_t const * city = L"Abc"; DWORD_PTR args[] = { (DWORD_PTR)name, (DWORD_PTR)city }; LPWSTR rawBuffer = nullptr; DWORD flags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_STRING | FORMAT_MESSAGE_ARGUMENT_ARRAY; DWORD result = FormatMessage(flags, message, 0, 0, (LPWSTR)&rawBuffer, 64, (va_list *)args); if (result != 0) { // Immediately wrap buffer to ensure it is safely freed unique_ptr<WCHAR, LocalFreeDelete<WCHAR>> p(rawBuffer); ++Counter::N; wcout << L"Formatted string: " << p.get() << endl; // ... now do something with the buffer ... } } int wmain(int argc, wchar_t ** argv) { FormatSomeMessage(); wcout << L"Final count: " << Counter::N << endl; return 0; }
The output shows that we have indeed freed the buffer on exit of the scope:
{ Enter FormatSomeMessage Formatted string: Name: Xyz / City: Abc LocalFree } Leave FormatSomeMessage Final count: 0
Multiple owner ref-counting
Alas, not every object is uniquely owned. Just as success has a thousand fathers, some objects have more than one logical parent. To avoid orphaning the memory, shared_ptr
is the tool for this situation. It internally maintains a reference count to ensure that the final instance that goes out of scope will destroy the object. Here is an example which uses the PPL:
#include <Windows.h> #include <sstream> #include <vector> #include <ppltasks.h> #include <concurrent_queue.h> using namespace std; using namespace concurrency; // ... void StartParallelTasks(vector<task<void>> & tasks, concurrent_queue<wstring> & queue) { FuncTrace t(__FUNCTIONW__); MyClassSPtr sp = make_shared<MyClass>(L"shared-obj"); for (int i = 0; i < 5; ++i) { task<void> task = create_task([sp, &queue]() { Sleep(500); DWORD id = GetCurrentThreadId(); wstringstream wss; wss << L"T:" << id << L":" << sp->get_Name(); queue.push(wss.str()); }); tasks.push_back(task); } } void StartAndWaitForTasks() { FuncTrace t(__FUNCTIONW__); vector<task<void>> tasks; concurrent_queue<wstring> queue; StartParallelTasks(tasks, queue); task<void> allTasksCompleted = when_all(tasks.begin(), tasks.end()).then([&queue]() { for_each(queue.unsafe_begin(), queue.unsafe_end(), [](wstring & s) { wcout << s << endl; }); }); allTasksCompleted.wait(); } int wmain(int argc, wchar_t ** argv) { StartAndWaitForTasks(); wcout << L"Final count: " << Counter::N << endl; return 0; }
The output shows that even though the initial shared_ptr
instance is long gone, the copies which were passed to the task lambda (via the capture clause) keep the object alive until the last task finishes:
{ Enter StartAndWaitForTasks { Enter StartParallelTasks MyClass:shared-obj } Leave StartParallelTasks T:27200:shared-obj ~MyClass:shared-obj T:34344:shared-obj T:39640:shared-obj T:40036:shared-obj T:40372:shared-obj } Leave StartAndWaitForTasks Final count: 0
Passing an object to a background thread
Okay, so let’s say you’re not using the PPL, but explicit threads instead. In this case, using std::thread
and shared_ptr
is the way to go:
#include <Windows.h> #include <thread> // ... thread CreateMyThread() { FuncTrace t(__FUNCTIONW__); MyClassSPtr sp = make_shared<MyClass>(L"used-by-thread"); return thread([](MyClassSPtr sp) { Sleep(1000); DWORD id = GetCurrentThreadId(); wcout << L"Thread " << id << L" using " << sp->get_Name() << endl; }, sp); } void CreateAndWaitForMyThread() { FuncTrace t(__FUNCTIONW__); thread myThread = CreateMyThread(); myThread.join(); } int wmain(int argc, wchar_t ** argv) { CreateAndWaitForMyThread(); wcout << L"Final count: " << Counter::N << endl; return 0; }
The output shows a similar situation as in the previous example where the object goes out of scope and is deleted shortly after the thread ends:
{ Enter CreateAndWaitForMyThread { Enter CreateMyThread MyClass:used-by-thread } Leave CreateMyThread Thread 41764 using used-by-thread ~MyClass:used-by-thread } Leave CreateAndWaitForMyThread Final count: 0
Passing an object to a background thread (legacy)
Fine, you don’t want to use std::thread
. You can modify the above sample to use the raw Win32 calls (e.g. CreateThread
, WaitForSingleObject
, CloseHandle
) but it is somewhat more error prone. Basically, you need to ensure cleanup happens inside the thread on success or before you return/throw on failure.
#include <Windows.h> // ... DWORD WINAPI MyLegacyThreadFunc(void * obj) { MyClassUPtr p(static_cast<MyClass *>(obj)); Sleep(1000); DWORD id = GetCurrentThreadId(); wcout << L"Thread " << id << L" using " << p->get_Name() << endl; return 0; } HANDLE CreateMyLegacyThread() { FuncTrace t(__FUNCTIONW__); MyClassUPtr p = make_unique<MyClass>(L"used-by-thread"); HANDLE handle = CreateThread(nullptr, 0, MyLegacyThreadFunc, p.get(), 0, nullptr); if (!handle) { throw runtime_error("Failed!"); } // Must release to avoid double-delete! p.release(); return handle; } void CreateAndWaitForMyLegacyThread() { FuncTrace t(__FUNCTIONW__); HANDLE myThread = CreateMyLegacyThread(); WaitForSingleObject(myThread, INFINITE); CloseHandle(myThread); } int wmain(int argc, wchar_t ** argv) { CreateAndWaitForMyLegacyThread(); wcout << L"Final count: " << Counter::N << endl; return 0; }
As expected, the thread will clean up the resource if it starts properly:
{ Enter CreateAndWaitForMyLegacyThread { Enter CreateMyLegacyThread (Dynamic allocation) MyClass:used-by-thread } Leave CreateMyLegacyThread Thread 33656 using used-by-thread ~MyClass:used-by-thread (Dynamic deallocation) } Leave CreateAndWaitForMyLegacyThread Final count: 0
That should be enough examples to give you an idea of smart ways to manage memory (or really, object ownership) in C++. Adoption of these kinds of modern coding practices should ensure safer, cleaner, less leaky applications.