Templates for Efficient Dynamic Type Checking

Here's a handy template that makes it easy to check your casts without losing performance.


November 01, 1999
URL:http://www.drdobbs.com/templates-for-efficient-dynamic-type-che/184403724

November 1999/Templates for Efficient Dynamic Type Checking

Here's a handy template that makes it easy to check your casts without losing performance.


Casting is unavoidable with today's application frameworks. It is also a known source of bugs. To help make casting safer, I have written assert_cast, a set of C++ function templates. These functions perform dynamic type checking in debug mode, without introducing any overhead in the release build, and without the extra clutter of macros like MFC's ASSERT_KINDOF. A sample usage with MFC is as follows:

CMyView* pView = assert_cast<CMyView*>(GetActiveView());

The above statement verifies that the returned "active view" is really a kind of CMyView.

I've implemented assert_cast as a pair of overloaded function templates (see Listing 1, cast.h). The compiler deduces the source type from the function argument and chooses one of the two overloads. The compiler cannot deduce the result type from the argument, so you must specify that explicitly (as in the MFC example above).

The overloading is needed because dynamic_cast has different semantics for pointers and references. For pointers, a failed dynamic_cast returns a null pointer, while for references, it throws std::bad_cast.

In the pointer case, assert_cast wraps the dynamic_cast with the assert macro. When NDEBUG is defined, the macro expands to nothing, the dynamic_cast is never evaluated, and the entire function can be inlined to a simple static_cast. On the other hand, when NDEBUG is not defined, the validity of the cast is checked, using RTTI (Run-Time Type Identification) when needed. If the cast is invalid, the equality test fails and the program aborts with a diagnostic [2].

The reference case is similar, but in this case the code catches exceptions explicitly and converts them to assertions. In both cases, the results of dynamic_cast and static_cast are compared to trap the occasional perverse situation in which dynamic_cast succeeds but static_cast produces a different and incorrect result [3]. If an assertion trips, you can then go in with the debugger, examine the call stack at the moment of termination, and locate the offending cast.

Note that I've used NDEBUG and assert, rather than _DEBUG and ASSERT. The latter are not part of the C++ Standard, and there's no need for them here. Many development environments (including MSVC) will automatically define NDEBUG when appropriate, but check yours before you rely on it. NDEBUG must be consistently defined (or not defined) for all the files in a program; otherwise, violations of the ODR (One Definition Rule) [4] may result.

I designed assert_cast to use assertions instead of exceptions for several reasons. First, I wanted it to be usable even in environments where exceptions are considered unacceptable. Second, I wanted it to have no run-time overhead; this precludes checking in the release build. If exceptions were thrown only in the debug build, a program might inadvertently come to depend on handling them, only to fail when the checking is removed. Finally, any program behavior that differs between debug and release builds can be a source of insidious bugs that elude detection during testing, emerge in the field, then mysteriously vanish again when brought home to the debugger. Assertions can't be "handled," so they avoid this problem.

Workarounds

The templates in Listing 1 look innocuous enough; unfortunately, in practice they can put a strain on a compiler's template argument deduction and overload resolution capabilities. Of the three C++ compilers I tried (Intel 4.0, gcc 2.95, MSVC 6), only the Intel could digest my test module. For the others, various hacks are necessary.

For gcc, my workaround was to use a class template that feels like a function template. Then I used partial specialization on the result type to distinguish reference casts from pointer casts. (Function templates do not support partial specialization, although a similar technique, partial ordering, can often be used to get the same effect.) With the workaround, the MFC example above would actually construct a temporary object of type assert_cast<CMyView *>, then automatically invoke a conversion operator to return a value of type CMyView *, which would be assigned to pView. The code to implement this is not particularly readable, but normally you don't need to be concerned with the internal details of the template. In practice it's quite efficient and easy to use. The code is shown in Listing 2, alt_cast.h.

I declare (but do not define) the general version of assert_cast, so the compiler will understand the specializations that follow. Then I define a specialization for pointer casts. The constructor for assert_cast is itself a template function, which converts the source to the result type via static_cast. (At this point, the compiler will reject any casts that don't qualify for static_cast, such as conversions between unrelated types.) The result of the conversion is stashed away in m_pResult.

The body of the constructor handles the actual dynamic type checking, using essentially the same assertion as the code in Listing 1. If the cast is valid, the constructor returns and operator Result* is automatically invoked, returning the pointer stashed in m_pResult.

The specialization for references works similarly. The reference is converted to a pointer for dynamic_cast, since pointer casts have more convenient semantics.

The workaround in Listing 2 operates transparently on both pointers and references. Unfortunately, it must rely on template partial specialization, which some compilers (notably MSVC) do not support. For those compilers, an alternative workaround is to rename the function template for references (from Listing 1) to, say, assert_ref_cast. Since there's no overloading, there are no ambiguities, but the user must remember two different function names, which is a little inconvenient.

If you're not sure if your compiler needs a workaround, you can download the file democast.cpp from the CUJ ftp site (see p. 3 for downloading instructions) and try compiling it. If you get no errors, you should be able to use the code from Listing 1. If your compiler chokes, download alt_cast.h, uncomment the line #include "alt_cast.h" in democast.cpp, and try again. If your compiler still chokes, you'll have to rename one of the templates in cast.h.

Usage

So, when should assert_cast be used? First, you might consider whether your program can be designed to avoid casting. Dynamic type checking is "safe" in the sense that it does catch type errors, but the program must first be running. Static typing, on the other hand, catches errors at compile time. This is faster and more reliable. Templates can often eliminate the need for casting, or at least hide it behind a safe interface. (You won't find a single dynamic_cast in the entire STL.)

When casting can't be avoided, assert_cast becomes an option. For class hierarchy navigation, assert_cast is more robust than either static_cast or the C-style cast. Given a polymorphic type, assert_cast can do downcasts as well as the occasional upcast or cast to a sibling type.

assert_cast does have some limitations. It won't let you accidentally or deliberately cast away constness. Unlike plain dynamic_cast, it cannot do cross-casts and it cannot cast from a virtual base.

The most common use for assert_cast is in working with frameworks. These frameworks offer services in terms of the frameworks' own types, but users need to operate on their own derived types. To make the conversion, a downcast is required. The MFC code above is an example of this usage. In such cases, dynamic_cast is also an option, but assert_cast will produce smaller, faster code. I have found that since I don't need to worry about run-time overhead with assert_cast, I use it more regularly than I would have used dynamic_cast, and as a result my programs are more thoroughly checked.

References

[1] To be more precise, the example assumes that CMyView is derived from CView, a polymorphic type. GetActiveView returns a CView *. If the returned pointer actually references an object of type CMyView or any type derived from CMyView, the cast will succeed and a valid CMyView * will be returned. On the other hand, if the object is not a kind of CMyView, an assertion will be raised.

[2] The Standard specifies a call to std::abort, but some implementations will trigger a debug break instead.

[3] For an example, see the diamond-shaped hierarchy described by Bjarne Stroustrup in The Design and Evolution of C++ (Addison-Wesley, 1994), section 14.3.2.1.

[4] The One-Definition Rule requires that, in a given program, all definitions of a non-local name must be identical. Since the assert macro expands differently depending on whether NDEBUG is defined, the ODR is easily violated if assert is used in inline functions (such as the assert_cast constructor) and NDEBUG is not consistently defined.

Ivan J. Johnson is a software engineer and consultant in Sacramento, CA. He can be reached at [email protected].

November 1999/Templates for Efficient Dynamic Type Checking/Listing 1

Listing 1: The assert_cast function templates

// cast.h  definitions for assert_cast

#ifndef CAST_H_A2434B93
#define CAST_H_A2434B93

#include <assert.h>

// assert_cast is similar to dynamic_cast; it differs in that 
// (1) RTTI, with its (small) associated cost in executable size 
// and speed, is used only in Debug mode (i.e., when NDEBUG is 
// not #defined), and (2) a bad cast is signalled by raising an 
// assertion, rather than by returning 0 or throwing 
// std::bad_cast.

// for pointers
template<typename Result_ptr, typename Source>
inline Result_ptr assert_cast(Source* pSource)
{
    assert ( static_cast<Result_ptr>(pSource) ==
                 dynamic_cast<Result_ptr>(pSource) );
    return static_cast<Result_ptr>(pSource);
}

// for references
template<typename Result_ref, typename Source>
inline Result_ref assert_cast(Source& rSource)
{
#ifndef NDEBUG
    try
    {
        assert ( &static_cast<Result_ref>(rSource) ==
                    &dynamic_cast<Result_ref>(rSource) );
    }
    catch(...)
    {
        // convert exceptions to assertions to prevent them 
        // from being "handled"
        assert(false);
    }
#endif
    return static_cast<Result_ref>(rSource);
}

#endif  // CAST_H_A2434B93
November 1999/Templates for Efficient Dynamic Type Checking/Listing 2

Listing 2: Template class workarounds for compiler template argument deduction and overload resolution problems

// alt_cast.h  definitions for assert_cast (workarounds for 
// certain compilers)

#ifndef CAST_H_A2434B93
#define CAST_H_A2434B93

#include <assert.h>

// declare general version
template<typename Result>
class assert_cast;

// define specialization for pointers
template<typename Result>
class assert_cast<Result*>
{
public:
    template<typename Source>
    assert_cast(Source* pSource)
        : m_pResult( static_cast<Result*>(pSource) )
    {
        assert ( m_pResult ==
                     dynamic_cast<Result*>(pSource) );
    }

    operator Result*() const { return m_pResult; }

private:
    Result* m_pResult;
};

// define specialization for references
template<typename Result>
class assert_cast<Result&>
{
public:
    template<typename Source>
    assert_cast(Source& rSource)
        : m_rResult( static_cast<Result&>(rSource) )
    {
        assert( &m_rResult );
        assert( &m_rResult ==
            dynamic_cast<Result*>(&rSource) );
    }

    operator Result&() const { return m_rResult; }

private:
    Result& m_rResult;
};

#endif  // CAST_H_A2434B93

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.