Writing Win32 apps like it's 2020: Helpers for a modern C++ world
- Introduction
- Helpers for a modern C++ world
- A DPI-aware resizable wizard
We are now going to get into the nitty-gritty details of Win32 and how modern C++ can help us here.
Contents
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::string
s 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::vector
s 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:
- 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. - 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. - The return value of
LoadStringW
isn’t checked at all, hencewszString
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:
- Use the last parameter of
CreateWindowExW
to pass the pointer to our class instance. This pointer is then available in theWM_CREATE
andWM_NCCREATE
messages, two messages that are sent early when a window is created. - Retrieve the pointer in
WM_NCCREATE
and store it viaSetWindowLongPtrW
for all subsequent window messages. - 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.