Greetings! I'm David Blaikie, software developer engineer at Microsoft and guest blogger here on VCBlog. I work in the Windows product group, writing test infrastructure tools in C++. One of the luxuries of the codebases I work in is that they are relatively modern and flexible. We use the full gamut of C++0x features implemented in Visual Studio 2010 including lambdas and rvalue references/move construction where possible. At a more fundamental level, we also choose to use exceptions.
Though I won’t get into a debate on the pros and cons of different error handling techniques in this post, it suffices to say that we use exceptions but both our dependencies in some cases (WinAPIs, etc) and some of our users (we have API level exposure to Windows test code sources) don’t expose exceptional APIs or build their code with exceptions enabled. To that end, we, like many other developers, need to live our exceptional lives within a possibly unexceptional world. Perhaps you’ve encountered similar situations and thought that because the rest of your code base (or your dependencies/consumers) weren’t using exceptions that your own code was just going to have to inherit that design choice.
In fact, there are a variety of simple techniques, reusable code snippets and small library classes you can write to happily insulate your exceptional code from unexceptional consumers and dependencies.
Unexceptional Dependencies
Dealing with dependencies (API functions, classes) that do not use exceptions is fairly simple. Let’s first take an example of some simple unexceptional code and see how it might be transformed:
- BOOL DiffHandles(HANDLE file1, HANDLE file2);
- BOOL DiffFiles(const wchar_t* file1, const wchar_t* file2)
- {
- HANDLE file1Handle = CreateFile(file1, GENERIC_READ, ?);
- BOOL result = FALSE;
- if (fileHandle != INVALID_HANDLE_VALUE)
- {
- HANDLE file2Handle = CreateFile(file2, GENERIC_READ, ...);
- if (file1Handle != INVALID_HANDLE_VALUE)
- {
- result = DiffHandles(file1Handle, file2Handle);
- CloseHandle(file2Handle);
- }
- CloseHandle(file1Handle);
- }
- return result;
- }
First things first: Don’t use this function. It’s good as a demonstration, but as real code it has a bunch of things wrong with it (not to mention it’s incomplete anyway).
While this code doesn’t seem to have a lot of error handling problems, that’s only because the error message handling is done off to the side. CreateFile, for example, specifies that if it returns INVALID_HANDLE_VALUE it will provide additional error information through GetLastError and we could imagine that DiffHandles would have to do the same thing, seeing similar failures from reading from the file (if a network share went away, for example) and would heap those all under a FALSE return value. Users of DiffFiles would have to ensure they check GetLastError any time that FALSE is returned to them. Not only would it be easy for them to forget and just assume the files were different, rather than that the comparison itself failed (perhaps it’s a transient network issue – the file comparison failed and then the network connection came up again and the files happen to have matching contents. This could cause problems) but also the failures the user of this API receives are vague and any messages shown to the user would be hard for the user to understand or address. While the error value from the function might say that a file was [not found, unable to be read because of permissions, or any other reason] it might not say /which/ of the two files had the problem.
So our first step might be to ensure actual failures produce exceptions while the test (are the file contents different) returns false. This will distinguish between the legitimate cases and runtime failures that occurred while attempting to execute the function. In this case our function’s type remains the same (though I’ll switch to “bool” now that we don’t have a Win32 legacy to interact so directly with) though the contract is different: Return true if the files match, false if the files differ, throw an exception if we couldn’t determine whether the files match or not.
We’ll introduce a simple helper function to that end:
- void ThrowLastErrorIf(bool expression, const wchar_t* message)
- {
- if (!expression)
- {
- throw Win32Exception(GetLastError(), message);
- }
- }
This isn’t the most advanced form of such a function. We could do much more to create more informative/human readable error messages. Perhaps Win32Exception type could use FormatMessage to produce a human readable message from the error code and append our message string on to that in some manner. In any case, our function can now be rewritten as follows:
- bool DiffFiles(const wchar_t* file1, const wchar_t* file2)
- {
- HANDLE file1Handle = CreateFile(file1, GENERIC_READ, ?);
- ThrowLastErrorIf(file1Handle != INVALID_HANDLE_VALUE, file1);
- HANDLE file2Handle = CreateFile(file2, GENERIC_READ, ...);
- ThrowLastErrorIf(file2Handle != INVALID_HANDLE_VALUE, file2);
- BOOL result = DiffHandles(file1Handle, file2Handle);
- ThrowLastErrorIf(result == FALSE && (GetLastError() != MY_APPLICATION_ERROR_FILE_MISMATCH), L"Could not compare file contents");
- CloseHandle(file1Handle);
- CloseHandle(file2Handle);
- return result;
- }
But wait, I (hope I) hear you cry, wouldn’t this leak file handles if we throw exceptions? Right you are. While I could’ve written this modified version to handle that leak it would’ve been somewhat convoluted so instead I’m going to demonstrate a better way.
By wrapping up these HANDLEs in a type that can handle their destruction in a more C++, RAII manner we can not only make this code more readable but also correct (non-leaking)
- class File
- {
- private:
- HANDLE handle;
- public:
- File(const wchar_t* file)
- {
- handle = CreateFile(file, GENERIC_READ, ...);
- ThrowLastErrorIf(handle, file);
- }
- HANDLE Get()
- {
- return handle;
- }
- ~File()
- {
- CloseHandle(handle);
- }
- //disabling coping to avoid double closing
- File& operator=(const File&) = delete;
- File(File&) = delete;
- };
And rewriting the original function again, we get:
- bool DiffFiles(const wchar_t* file1, const wchar_t* file2)
- {
- File f1(file1);
- File f2(file2);
- result = DiffHandles(f1.Get(), f2.Get());
- ThrowLastErrorIf(result == FALSE && (GetLastError() != MY_APPLICATION_ERROR_FILE_MISMATCH), L"Could not compare file contents");
- return result;
- }
All without leaks and the need to pay close attention to code paths to ensure resource destruction.
It’s not quite the same as it would be if DiffHandles was an exception-aware API, but it tidies up the function a bit and means that unexceptional dependencies don’t pollute our exception-aware codebase.
The implementation of the Win32Exception type and enhancements to the ThrowLastErrorIf function (to include mapping specific result values to already well known exception types such as std::bad_alloc) is left as an exercise for the reader.
Unexceptional Consumers
Types of Unexceptional Consumers
We’ve seen that Win32 “invalid return (FALSE, INVALID_HANDLE_VALUE, etc) + GetLastError” is one kind of unexceptional error message scheme. Other APIs you might run into that have an unexceptional boundary include C code (indeed Win32’s API is a specific case of this, but POSIX uses int return values and errno to similar effect) or HRESULT returning COM APIs.
[While it might not be immediately obvious why it’s worth considering C code as an unexceptional consumer (“my users will be writing in C, so my library must be in C” I hear you cry) it’s actually quite possible to write a C API in C++. By declaring your functions with extern “C” you can have C linkage functions in a C++ compilation unit using the full functionality of the C++ programming language in your implementation]
Dealing with Unexceptional Consumers
Dealing with unexceptional consumers is perhaps a little trickier, though the most basic implementation is not terribly difficult, if a little verbose and lossy. Let’s invert the above example. Imagine we had the original DiffFiles, but we wanted to keep the interface (BOOL do_things + GetLastError) but we had updated our dependencies (File handling using RAII resource wrappers as shown, as well as updating DiffHandles, or using the STL) to be exception-aware themselves. As such they no longer return failures through GetLastError, instead returning bool and throwing exceptions for their failures. Perhaps not even Win32Exception failures (this particular exception type wouldn’t be used pervasively, but only when interacting with unexceptional APIs where no more accurate exception type was available to represent the failure). We could simply rewrite the DiffFiles function as follows:
- BOOL DiffFiles(const wchar_t* file1, const wchar_t* file2)
- {
- try
- {
- File f1(file1);
- File f2(file2);
- if (!DiffHandles(f1, f2))
- {
- SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
- return FALSE;
- }
- return TRUE;
- }
- catch(const Win32Exception& e)
- {
- SetLastError(e.GetErrorCode());
- }
- catch(const std::exception& e)
- {
- SetLastError(MY_APPLICATION_GENERAL_ERROR);
- }
- return FALSE;
- }
You should be sure to catch any/all exceptions which could be produced by the code in the try block. In this case we know that the File class and DiffHandles function can only throw Win32Exceptions so we can just handle that.
With this basic implementation we lose all exception detail, even those details we could map to interesting results (perhaps std::bad_alloc could be mapped to an out of memory Win32 error code for example), so it’s not ideal. Again, we could imagine putting a variety of catch blocks in to map different exception types to various failures, adding logging to record the full details of the exception (since we’ll be compressing entire exception objects including strings of context, stack traces, etc, into a single win32 error code) before it is coalesced into a single value for return, etc. In doing so every one of the functions on our unexceptional public interface is going to get long and unwieldy.
Macros as an Exception Consuming Boundary
To reduce the syntactic overhead in this case we can use macros to implement a convenient wrapper to hide all that possible complexity and repeated logic:
- #define WIN32_START try {
- #define WIN32_END } catch (const Win32Exception& e) { SetLastError(e.GetErrorCode()); } catch (const std::exception& e) { SetLastError(MY_APPLICATION_GENERAL_ERROR); } return FALSE;
The do_things function then becomes:
- BOOL DiffFiles(const wchar_t* file1, const wchar_t* file2)
- {
- WIN32_START
- File f1(file1);
- File f2(file2);
- if (!DiffHandles(f1, f2))
- {
- SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
- return FALSE;
- }
- return TRUE;
- WIN32_END
- }
Lambdas as an Exception Consuming Boundary
We can tidy this up a little further, replacing macros with lambdas as follows:
- template<typename Func>
- BOOL Win32ExceptionBoundary(Func&& f)
- {
- try
- {
- return f();
- }
- catch(const Win32Exception& e)
- {
- SetLastError(e.GetErrorCode());
- }
- catch(const std::exception& e)
- {
- SetLastError(MY_APPLICATION_GENERAL_ERROR);
- }
- return FALSE;
- }
With this function, we can now reduce our do_things() function to:
- BOOL DiffFiles(const wchar_t* file1, const wchar_t* file2)
- {
- return Win32ExceptionBoundary([&]()
- {
- File f1(file1);
- File f2(file2);
- if (!DiffHandles(f1, f2))
- {
- SetLastError(MY_APPLICATION_ERROR_FILE_MISMATCH);
- return FALSE;
- }
- return TRUE;
- });
- }
The Win32ExceptionBoundary could be generalized (so it could be used with, say, HANDLE returning functions) by taking the error result as an extra parameter and using that to infer the return type of the template function, for example.
Summary
With tools such as these you can introduce exception-aware code into your code base, enabling you to take advantage of the myriad of carefully implemented and tested standard libraries such as containers, smart pointers, and algorithms without having to revamp your entire codebase. Your exception-aware walled garden can grow as time and business justification permits, converting single functions/libraries at a time.
Rachel Blanchard Sienna Guillory Tricia Vessey Aki Ross Ashley Tappin
No comments:
Post a Comment