Writing Win32 apps like it's 2020: Helpers for a modern C++ world

This is the second part of a three-part series on Win32 development:


Example Project on GitHub

We are now going to get into the nitty-gritty details of Win32 and how modern C++ can help us here.

Pointers that free themselves

If you are coming from the C world, you have been managing all your resources manually so far. You allocate each heap memory block using malloc and need to take care of a call to free in every exit path of the function. As you have probably observed already, this easily leads to human mistakes: A forgotten free in just one exit path is enough to cause a severe memory leak. Freeing a pointer twice or using it after a free happens just as easy and can be the source of very painful bugs.

Being a C API, Win32 is no different in this regard. It offers you HeapAlloc and HeapFree, which are a bit closer to the operating system than their C library counterparts, but otherwise suffer from the same issues.

Now you may argue that YOU obviously never make such mistakes. So let’s just talk about the code you import from someone else. If you see a manual memory allocation there, how can you be sure that the programmer correctly handled all exit paths? There is no way to ensure that unless you check the entire code yourself.

C++ has been trying to be better at memory management since its infancy. But it has taken until 2011 for C++ to finally come up with a reliable solution that is also going to work tomorrow: std::unique_ptr
This template class is a wrapper around any object that you want to put on the heap. It allocates memory for the wrapped object in its constructor and frees it in its destructor. As C++ automatically calls the destructor whenever the std::unique_ptr goes out of scope, you can no longer forget that and the human is taken out of the equation. This makes std::unique_ptr a so-called smart pointer.

If you have been into C++ for quite a while, you may know std::unique_ptr’s predecessor std::auto_ptr. This one has been deprecated for good (and entirely removed in newer C++ standards) due to its unclear ownership semantics. The details don’t matter here, just keep in mind that std::unique_ptr fully owns an object and can’t accidentally lose it. Unlike std::auto_ptr, you can only transfer ownership of that object by using an explicit std::move.

Note that std::unique_ptr is only an option for allocating a single object. It does not work for arrays, because those need to be freed differently (delete[] vs. delete). While people have often asked for an “auto_array_ptr”, modern C++ offers something even better, which we will dive into in the next section.

Universal C++ containers

For a long time, C char arrays and C++ std::string have been two different worlds. You could get a constant representation of a char array from an std::string using the c_str method, but a true two-way interaction between both types wasn’t possible. As a result, legacy C interfaces like the Win32 API often limited you to using traditional C-style arrays without the benefits of modern C++ containers. You had fun managing all the memory allocations yourself, making sure that nothing leaks, and doing strlen over and over again because a char array doesn’t know its size.

This has changed for good starting with C++11, which guarantees that an std::string is internally stored as a char array. C++17 adds a data method returning a writable pointer to that char array, finally making std::strings universally usable in all places that use char arrays.

Extensive and error-prone code like

const char* szTest = "Hello";
int cch = MultiByteToWideChar(CP_ACP, szTest, -1, NULL, 0);

const WCHAR* wszTest = HeapAlloc(GetProcessHeap(), 0, cch * sizeof(WCHAR));
if (!wszTest)
{
    printf("Out of memory!\n");
    return 1;
}

MultiByteToWideChar(CP_ACP, szTest, -1, wszTest, cch);

// Do something with wszTest
// Take care to clean it up in all exit paths using:

HeapFree(GetProcessHeap(), 0, wszTest);

can finally become as simple as

std::string strTest = "Hello";
int cch = MultiByteToWideChar(CP_ACP, strTest, strTest.size(), nullptr, 0);

std::wstring wstrTest;
wstrTest.resize(cch);
MultiByteToWideChar(CP_ACP, strTest, strTest.size(), wstrTest.data(), cch);

// Do something with wstrTest, cleanup happens automatically

By the way, the same is also true for std::vector. All your manual allocations of arrays, prone to buffer overflows and memory leaks, can finally be replaced by automatically managed std::vectors and still interact with APIs written in C.

String resources without regrets

Let’s get deeper into Win32 specifics and use what we just learned.
If you are writing applications for Windows, you have probably made good use of resources already. Resources allow you to organize strings and graphics at a central location separated from your code. Each resource is associated with a language code. When running your application, Windows will pick the resource that best matches your operating system language setting.

For loading a Unicode string resource, the Win32 API of choice is the LoadStringW function. Even if Microsoft sample code still does, you don’t want to use the ANSI counterpart LoadStringA and neither the LoadString macro that forwards to one of both. Both were last needed in the era of Windows 95/98/Me, which didn’t come with native Unicode support. The NT line of Windows has always supported Unicode from the kernel up to the applications. All A functions there are implemented to convert the string to Unicode and then call the corresponding W function. As we’re not targeting anything older than Windows XP here, we can (and should!) safely forget about A functions. Some newer Windows APIs don’t even provide them anymore.

I have seen most people using LoadStringW by declaring a buffer that is “hopefully large enough” and letting the function copy the string into that buffer. Even Microsoft sample code usually looks like this:

WCHAR wszString[100];
LoadStringW(hInstance, IDS_STRING_ID, wszString, sizeof(wszString) / sizeof(WCHAR));

This code is potentially dangerous in multiple dimensions:

  1. If the string for a language later turns out to be longer than 99 characters (consider the terminating NUL), it is silently truncated. In the best case, this only impacts your user experience badly. In the worst case, the user loses critical information because the string is later fed to a formatting function like printf and now misses a format character.
  2. The developer may forget to divide by the size of a WCHAR in the last parameter, thereby creating the illusion of a larger buffer and laying the foundation for a typical buffer overflow.
  3. The return value of LoadStringW isn’t checked at all, hence wszString may be uninitialized. If you later work with that buffer, expect things to go tremendously wrong.

Only few people know about the other (scarcely documented) way of using LoadStringW:

const WCHAR* pString;
int CharacterCount = LoadStringW(hInstance, IDS_STRING_ID, reinterpret_cast<LPWSTR>(&pString), 0);

Instead of copying the resource string into a buffer, this call provides you with a read-only pointer to it. All resources are already loaded into memory when starting your application, so retrieving the string this way performs no additional copying. However, there is a caveat here: The string is not necessarily NUL-terminated when being loaded this way. This makes it hard to use the string directly in a pure C world where NUL-terminated strings are expected everywhere. While we also get the character count from this LoadStringW call, we cannot just insert the missing NUL terminator ourselves due to the read-only nature of the string pointer.

Fortunately, C++ comes to the rescue here. As we have learned that its std:: classes are full-fledged alternatives to C-style character arrays these days, we can use them to fix our trouble with LoadStringW. Enough said, the final function looks like this:

std::wstring LoadStringAsWstr(HINSTANCE hInstance, UINT uID)
{
    PCWSTR pws;
    int cch = LoadStringW(hInstance, uID, reinterpret_cast<LPWSTR>(&pws), 0);
    return std::wstring(pws, cch);
}

These 3 lines are sufficient to solve all of our problems above. It doesn’t matter if the string is 10 or 1000 characters long, the function will transparently handle this. It also doesn’t care about any terminating NUL character, because it just initializes a new std::wstring and copies the retrieved number of characters from the read-only resource pointer. The returned std::wstring is guaranteed to be initialized and a comfortable and safe way to work with the loaded string.

I’m also using Hungarian notation in my final function to emphasize a few things. The pws abbreviation refers to a pointer to a wide-string. You may have more commonly seen pwsz, which would translate to a pointer to a wide-string terminated by zero. As we just discussed, the latter part does not apply here, and this is underlined by the subtle difference in naming. Similarly, cch stands for count of characters and distinguishes the integer variable from a count of bytes (cb). You will deal with both when using Win32 API, and proper naming helps to not mess things up.

Mastering the handle mess

If you write your Win32 application in C, you soon have handles all over the place. Every I/O operation, network connection or registry key, just to name a few, is identified by a handle variable that you must take care of. When a resource is no longer needed, you need to close the handle manually - and again you must do so for all exit paths of your function. This technique is susceptible to the same human mistakes that we already discussed above.

How can C++ help us here? Object-oriented frameworks like MFC and .NET tackle this problem by providing wrapper classes around every single handle type. They also wrap each related Win32 API into a class method in the hope that you never need to go the Win32 route again. In theory, this gives you a nice set of classes with automatic cleanups and hence no possibility of resource leaks. In practice, you still go the Win32 route as soon as a new Win32 API appears that has not been picked up by the framework yet. And if you ever wondered why these are called heavyweight frameworks, now you get an idea.

Can we also prevent any handle leaks without writing lots of code for every handle type? Yes, using modern C++!
The scope_guard is a template class that needs to know about your handle, its cleanup function, and the value indicating invalidity. Using just that information, it can wrap any resource and do proper cleanup whenever that resource goes out of scope. This scope_guard concept is currently a draft that may become part of a future C++ standard. Nevertheless, you can already use it now, for example by importing this lightweight header-only implementation into your code. After doing that, a registry key handle that automatically cleans up itself is just 3 lines away:

static inline auto make_unique_hkey(HKEY hKey)
{
    return sr::make_unique_resource_checked(hKey, nullptr, RegCloseKey);
}

The same technique works for Win32 file I/O handles:

static inline auto make_unique_handle(HANDLE h)
{
    return sr::make_unique_resource_checked(h, INVALID_HANDLE_VALUE, CloseHandle);
}

By the way, a notable exception here are HWND handles. You may think that they suffer from the same problem, as every UI control (button, edit box, etc.) receives such a handle. However, you need to pass a parent window when creating any UI control. Windows internally keeps track of all controls belonging to a certain window and cleans them up automatically as soon as the window is destroyed. This way, you cannot have any resource leaks here. Just remember to always destroy a window by calling the DestroyWindow function and you are good to go.

Gracefully failing constructors

Now that you know how to make your life easier when dealing with handles, you still need to get some order into variables scattered all over the place. Therefore, you create a class for every GUI window and encapsulate the corresponding handles in there. Good idea!

Creating a GUI window requires calling the Win32 APIs RegisterClassW and CreateWindowExW. You want to put these calls into the constructor of your class, because an instance of your class isn’t usable before this initialization has happened. However, these two Win32 APIs may fail. What do you do when you observe a failure here? A traditional C++ constructor has no way of returning an error status instead of a class instance.

The official C++ FAQ recommends throwing an exception in this case, but that would introduce a second error handling technique. Not really what we want considering that we can’t rewrite our entire application to use only exceptions. We still need to handle returned error codes from Win32 APIs.
The other alternative proposed in the FAQ isn’t any better: Requiring users of your class to call a second initialization function after creating an instance increases the complexity of your class and makes it less convenient to use. Assuming you do this, you would have to check in every member function whether the initialization function has already been called. Even the simplest member functions would need a return value to possibly indicate that initialization hasn’t happened yet. Just leaving out those checks and hoping that the user of the class will always call initialization is no option either. It’s rather the source for the next row of subtle bugs.

A proper solution would make it impossible to use a class instance without initialization, while also performing the required error handling during initialization. Is that possible with C++? Of course it is! All you need is a Named Constructor.
A Named Constructor is nothing more than a static class function that performs the necessary initialization and returns a new instance of the class. After implementing such a static class function, you can make the actual constructor of your class private. This makes it impossible for a user of your class to create a new instance without going through the Named Constructor.

A Named Constructor for a GUI window class CMainWindow may look like this:

std::unique_ptr<CMainWindow> CMainWindow::Create(HINSTANCE hInstance, int nShowCmd)
{
    // Register the main window class.
    WNDCLASSW wc = { 0 };
    wc.lpfnWndProc = CMainWindow::_WndProc;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
    wc.hIcon = LoadIconW(hInstance, MAKEINTRESOURCEW(IDI_ICON));
    wc.lpszClassName = CMainWindow::_wszWndClass;

    if (RegisterClassW(&wc) == 0)
    {
        std::wstring wstrMessage = L"RegisterClassW failed, last error is " + std::to_wstring(GetLastError());
        MessageBoxW(nullptr, wstrMessage.c_str(), wszAppName, MB_ICONERROR);
        return nullptr;
    }

    // Create the main window.
    auto pMainWindow = std::unique_ptr<CMainWindow>(new CMainWindow(hInstance, nShowCmd));
    HWND hWnd = CreateWindowExW(
        0,
        CMainWindow::_wszWndClass,
        wszAppName,
        WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        CW_USEDEFAULT,
        nullptr,
        nullptr,
        hInstance,
        pMainWindow.get());
    if (hWnd == nullptr)
    {
        std::wstring wstrMessage = L"CreateWindowExW failed for CMainWindow, last error is " + std::to_wstring(GetLastError());
        MessageBoxW(nullptr, wstrMessage.c_str(), wszAppName, MB_ICONERROR);
        return nullptr;
    }

    return pMainWindow;
}

As you see, it performs the necessary initialization steps and returns a nullptr if one of them fails (after also showing an error message to the user). It also returns the class instance as an std::unique_ptr to ensure that the caller cannot forget to free the associated memory. Creating the class instance on the heap enables us to pass an instance pointer to CreateWindowExW, one that is also valid after returning from the function. But more about that in the next section.

The only WndProc you’ll ever need

The attentive reader has already noticed that I’m passing a function pointer to CMainWindow::_WndProc in the previous code example. Every Win32 GUI window needs a WndProc function to process messages directed at it, such as button clicks, window resizing or focus changes. It can decide to either handle these messages on its own or pass them down to the default window procedure DefWindowProcW.

As we have just encapsulated everything we need for our window into a C++ class, we also want our WndProc to be a member function of that class. However, Win32 was designed for applications written in C and therefore cannot call a member function of a C++ class instance directly. It lacks a way to pass the class instance this pointer as the first parameter of every WndProc call. Instead, Win32 is hardwired to a WndProc having the following function signature:

LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

To work around this limitation, we have to resort to a little trick:

  1. Use the last parameter of CreateWindowExW to pass the pointer to our class instance. This pointer is then available in the WM_CREATE and WM_NCCREATE messages, two messages that are sent early when a window is created.
  2. Retrieve the pointer in WM_NCCREATE and store it via SetWindowLongPtrW for all subsequent window messages.
  3. Retrieve the pointer via GetWindowLongPtrW in all other window messages.

As this is a recurring and cumbersome pattern, it is best isolated into its own function. We use a template function here to support various kinds of window classes:

template<class T, class U, HWND (U::*m_hWnd)> T*
InstanceFromWndProc(HWND hWnd, UINT uMsg, LPARAM lParam)
{
    // Get the pointer to the class instance.
    T* pInstance;
    if (uMsg == WM_NCCREATE)
    {
        // The pointer has been passed via CreateWindowExW and now needs to be saved via SetWindowLongPtrW.
        LPCREATESTRUCT pCreateStruct = reinterpret_cast<LPCREATESTRUCT>(lParam);
        pInstance = reinterpret_cast<T*>(pCreateStruct->lpCreateParams);
        SetWindowLongPtrW(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(pInstance));

        // We are handling the first useful window message, so this is the perfect place to also set the hWnd member variable.
        pInstance->*m_hWnd = hWnd;
    }
    else
    {
        // Get the pointer saved via SetWindowLongPtrW above.
        pInstance = reinterpret_cast<T*>(GetWindowLongPtrW(hWnd, GWLP_USERDATA));
    }

    return pInstance;
}

Using this function, our WndProc can be as simple as:

LRESULT CALLBACK
CMainWindow::_WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    CMainWindow* pMainWindow = InstanceFromWndProc<CMainWindow, CMainWindow, &CMainWindow::m_hWnd>(hWnd, uMsg, lParam);

    // The first WM_GETMINMAXINFO comes before WM_NCCREATE, before we got our CMainWindow pointer.
    if (pMainWindow)
    {
        switch (uMsg)
        {
            case WM_COMMAND: return pMainWindow->_OnCommand(wParam);
            case WM_CREATE: return pMainWindow->_OnCreate();
            case WM_DESTROY: return pMainWindow->_OnDestroy();
            case WM_DPICHANGED: return pMainWindow->_OnDpiChanged(wParam, lParam);
            case WM_GETMINMAXINFO: return pMainWindow->_OnGetMinMaxInfo(lParam);
            case WM_PAINT: return pMainWindow->_OnPaint();
            case WM_SIZE: return pMainWindow->_OnSize();
        }
    }

    return DefWindowProcW(hWnd, uMsg, wParam, lParam);
}

Declare that as a static function in the header of CMainWindow and voilà, you have a WndProc that is perfectly able to call member functions of your CMainWindow instance for handling each message. As a static function with the expected signature, it doesn’t break any contract with Win32.

This should really be the only WndProc you’ll ever need!

Conclusion

Phew, this part got a little longer than I expected. But if you have made it until here, you should now know a few modern C++ concepts that can make your Win32 development experience a breeze. Maybe my lines even prevent a few resource leaks or bad class designs from happening.

In the next part of this series, we are going to look at another aspect of modern Win32 development that is often overlooked or outright ignored, but which I consider very important for a smooth user experience in 2020.