The new C++ for the new Windows / Part 0 / Putting bugs into buckets

This is the first article in a new series about C++ for Windows. I would have liked to start this series with a somewhat more fun topic but it needs to be said. There are certain prerequisites for starting a project on the right track and one of those is defining the approach for dealing with run-time errors. Many writers avoid talking about error handling when it comes to C++ because there are different approaches and differing views on how it should be done. I would like to say that you can use whatever approach suites you. I must however prepare the way for the remainder of this series and without a consistent way of dealing with errors subsequent articles won’t make sense.

As my approach relies on the Standard C++ Library and in particular the Standard Template Library the use of C++ exceptions is a given. The challenge then is to come up with a rational strategy for handling run-time errors. First I’ll describe what is to be done with exceptions and then how run-time errors in the Windows API are handled.

The first rule of exception handling is to do nothing. Exceptions are for unexpected run-time errors. Don’t throw exceptions you expect to catch. That also means you must not use exceptions for expected failures. If there are no exception handlers then Windows automatically generates an error report, including a minidump of the crash, as soon as an exception is thrown. This provides a perfect snapshot of the state of your application without unwinding the stack. This helps you to get as close to the source of the error as possible when you perform postmortem debugging. If you sign up with the Windows Quality Online Services (Winqual) then Microsoft will even collect these error reports, categorize them and provide them to you at no charge. This is tremendously valuable.

The next step is to clearly distinguish between fatal run-time errors, those that will crash your program, and run-time errors that are expected and that you will handle in your program so that it can continue to run. Some of these will be unique to your application but many will be common to most applications. Keep in mind that unexpected run-time errors that will be reported with exceptions indicate two things. Firstly they may indicate a bug in your application. You assumed the contents of a file is in a certain format when it is not. You expected a folder to exist when it’s actually missing. You sent a message to a window that’s already destroyed. Your algorithm dereferences an invalid iterator. And so on. Secondly they indicate problems outside of your control. Some other factor on the computer causes memory allocations in your application to fail. You fail to get the size of your window’s client area. You fail to write a value to the registry. These types of run-time errors typically point to a bigger problem. In both cases you don’t want your application to continue and an exception that results in an error report is the fastest way to bring your application down so that it doesn’t cause any further harm and lets you debug the problem when the error report arrives.

On the other hand many errors can and should be handled by your application. You may expect writing a value to the registry to succeed but you probably shouldn’t expect reading a value to succeed. Parsing text should be expected to fail. Creating a file may fail if the complete directory structure isn’t already present. And so on. In these cases using exceptions is not usually appropriate. It is usually simpler and more efficient to handle the error directly and as close to the source of the failure as possible.

Now let’s turn our attention to the many functions in the Windows API and the various ways they report run-time errors. The Windows API is unfortunately not very consistent when it comes to reporting run-time errors. You can think of it as having islands of consistency in a sea of inconsistency. There are four common types used for reporting errors explicitly using a return value.

BOOL

Many functions return a BOOL, a typedef of int, indicating either success or failure. It is best to compare the result against 0 rather than 1 since some functions only guarantee that the result will be nonzero upon successful completion. Some but certainly not all of these functions will provide a more descriptive error code that you can retrieve using the GetLastError function upon failure. The values returned by GetLastError can usually be found in the winerror.h header file excluding the HRESULT values.

LONG/DWORD

Different libraries use various typedefs of long and unsigned long including LONG, DWORD, NET_API_STATUS and others to return an error code directly. In most cases success is defined by a 0 return value. These functions typically directly return error codes defined in the winerror.h header file excluding the HRESULT values.

HRESULT/NTSTATUS

Many newer libraries as well as most member functions of COM interfaces use an HRESULT return value to report errors. Some functions that have roots in the Windows Driver Kit use an NTSTATUS return value. Both of these define identical packed error codes. It is not uncommon for values from winerror.h, possibly returned by GetLastError, to be packed inside an HRESULT before returning it to the caller. An HRESULT or NTSTATUS value can have multiple values indicating success and of course multiple values indicating failure. You need to check the documentation for any function that you’re using but in most cases a 0 return value indicates success with negative values indicating failure. Additional values greater than 0 may be defined to distinguish between different variations of success.

A small number of functions have a void return type. This either means that the function cannot fail, usually because whatever resources it relies on have already been allocated, or that any failure will be reported at a later stage. Other return values often imply an error given some sentinel value. This is common in functions that return a handle or pointer to a resource. You just need to read the documentation carefully to determine how to distinguish success from failure as it is not always true that 0 or nullptr alone indicate failure.

Finally for all those internal assumptions in your application there are assertions. Prefer to use static_assert for compile time validation. When that’s not possible use an ASSERT macro that is compiled away in release builds. I prefer to use the _ASSERTE macro from crtdbg.h as it stops the program in the debugger right on the offending line of code.

The listing at the end of this article includes error.h and error.cpp used for error handling in subsequent articles.

Although I avoid macros as much as possible, they remain the only solution for implementing debug assertions. I also define VERIFY and VERIFY_ mainly for checking the result of functions called within destructors where exceptions should not be used. It at least lets me assert the result of these functions in debug builds.

Namespaces are used to partition types and functions unless they’re specifically designed to work together. The error handling functions are however so fundamental that they reside in root kerr namespace. A few overloaded functions are provided for checking the return value of most functions in the Windows API. Argument matching and integral promotion rules help to funnel the various return types into the appropriate check functions. Specifically the int overload handles bool and BOOL return types, the long and unsigned long overloads take care of the rest. The check template function also comes in quite handy when you need to check for a specific value rather than the usual logical success or failure return values.

The check functions throw check_failed exceptions. The check_failed type includes a member that holds the error code passed to the check functions or returned by GetLastError. This comes in handy when you receive a minidump which contains the address of the exception and then allows you to easily find this error code. This can often be invaluable in determining the cause of the crash.

Why did I title this part “Putting bugs into buckets”? Well that’s because Windows Error Reporting categorizes error reports into what they call buckets. And that’s all for today and as always I’d love to hear what you think.

error.h

#pragma once

#ifdef _DEBUG
    #include <crtdbg.h>
    #define ASSERT(expression) _ASSERTE(expression)
    #define VERIFY(expression) ASSERT(expression)
    #define VERIFY_(expected, expression) ASSERT(expected == expression)
#else
    #define ASSERT(expression) ((void)0)
    #define VERIFY(expression) (expression)
    #define VERIFY_(expected, expression) (expression)
#endif

namespace kerr
{
    struct check_failed
    {
        explicit check_failed(long result);

        long error;
    };

    void check(int);
    void check(long);
    void check(unsigned long);

    template <typename T>
    void check(T expected, T actual)
    {
        if (expected != actual)
        {
            throw check_failed(0);
        }
    }
}

error.cpp

#include "Precompiled.h"
#include "error.h"
#include <Windows.h> // for GetLastError

kerr::check_failed::check_failed(long result) :
    error(result)
{
    // Do nothing
}

void kerr::check(int result)
{
    if (!result)
    {
        throw check_failed(GetLastError());
    }
}

void kerr::check(long result)
{
    if (result)
    {
        throw check_failed(result);
    }
}

void kerr::check(unsigned long result)
{
    if (result)
    {
        throw check_failed(result);
    }
}

Next: Objects and handles

15 thoughts on “The new C++ for the new Windows / Part 0 / Putting bugs into buckets

    1. Kenny Kerr Post author

      That’s a good question. There is no mandate for this and indeed I see no benefit in doing so. Organizing exceptions into hierarchies can be useful when the exceptions are closely related; a group of mathematical errors perhaps. But using a single hierarchy for all exceptions doesn’t provide any benefit in C++. Other runtimes such as the CLR differ in this regard. Developers are sometimes tempted to do so as a means of achieving a consistent error handling or event tracing facility but there are far better approaches for achieving these goals in native code on Windows. I’ll be coving this in more detail in a future article in this series when I talk about diagnostics.

      Reply
  1. Cristian

    Thank you for promoting “precompiled.h”, it’s about time to get rid of “stdafx.h”.

    Regarding the assertions, I have done a comparison of your method, the portable c++ method, ATL, and, MFC methods.

    My test code is here: http://pastebin.com/P4DJ37Dh
    Assertion screenshots here: http://yfrog.com/48assertsp

    MFC doesn’t display the expression that failed the assertion.

    The portable c++ method differs a bit in the output message. I like this method because it doesn’t use caps letters! :)

    Reply
    1. Cristian

      Hmm, the portable c++ method displays assertions also in release builds, which means that one still needs an #ifdef block like you presented in the article.

      Reply
      1. Florin Crişan

        The standard assert macro, defined in () depends on the NDEBUG macro instead of Microsoft’s _DEBUG macro.
        Having said that, Visual Studio does define by default NDEBUG for release, in its .vcproj files

    2. Kenny Kerr Post author

      Thanks Cristian, I can’t stand stdafx either. :)

      I prefer to give macros SCARY_UPPERCASE names to avoid confusing them with “real code”.

      As for the choice of assert macro, ATL’s macros are very good so if you’re using ATL go ahead and use ATLASSERT, ATLENSURE et al. I don’t use MFC so that’s not an issue. The assert macro from cassert does display the failing expression but it breaks in the debugger in the wrong place. Ideally it should break on the failing expression itself and that’s exactly what _ASSERTE from crtdbg.h does.

      Reply
  2. CppLover

    Interesting article.

    BTW: What do you think of using ATL’s AtlThrowLastWin32?
    Your check_failed exception class embeds a 32-bit integer, like ATL’s CAtlException does for HRESULT (and it’s possible to convert from Win32 error code to HRESULT’s using HRESULT_FROM_WIN32).
    Thanks.

    Reply
    1. Kenny Kerr Post author

      If you’re using ATL then using AtlThrow and AtlThrowLastWin32 is not a bad idea. I however prefer the uniformity of the overloaded check functions and the fact that they package up the right error code in most cases. ATL isn’t very elegant in that regard.

      Reply
  3. Alain Rist

    That’s what I use with VC2010 for assert and verify. Resolves to the same but IMHO more C++ish :)

    /////////////////////////////////////////////////////////////////////
    // Assert and Verify

    template
    void Assert(V&& v)
    {
    static_assert(std::is_convertible::value, “V must be convertible to bool”);
    _ASSERTE(v);
    v;
    }

    void AssertSuccess(HRESULT hr)
    {
    Assert(SUCCEEDED(hr));
    }

    template
    V Verify(V&& v)
    {
    Assert(v);
    return v;
    }

    HRESULT VerifySuccess(HRESULT hr)
    {
    AssertSuccess(hr);
    return hr;
    }

    Reply
  4. Alain Rist

    Correct formatting of my previous post, sorry :(
    /////////////////////////////////////////////////////////////////////
    // Assert and Verify

    template<typename V>
    void Assert(V&& v)
    {
    static_assert(std::is_convertible<V, bool>::value, "V must be convertible to bool");
    _ASSERTE(v);
    v;
    }

    void AssertSuccess(HRESULT hr)
    {
    Assert(SUCCEEDED(hr));
    }

    template<typename V>
    V Verify(V&& v)
    {
    Assert(v);
    return v;
    }

    HRESULT VerifySuccess(HRESULT hr)
    {
    AssertSuccess(hr);
    return hr;
    }

    Reply

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s