Alternatives to dynamic_cast

Started by
13 comments, last by Hodgman 8 years, 4 months ago

Hi guys,

In his excellent book, 55 Specific ways to improve your programs and designs, Scott Meyers has the following to say about dynamic casting:

1.) "If you find yourself wanting to cast, it’s a sign that you could be approaching things the wrong way. This is especially the case if your want is for dynamic_cast." pg 120

2.) "many implementations of dynamic_cast can be quite slow" pg 120

3.) "Good C++ uses very few casts" pg 123

4.) "you should be especially leery of dynamic_casts in performance-sensitive code." pg 121

One of Scott's solutions is the following: "provide virtual functions in the base class that let you do what you need"

As I have up to this point relied heavily on dynamic_casts (especially the cascading types) I was wondering, given the following scenario...


class Person{};

class Student:public Person{};

class Employee:public Person{};

//later in the code


std::list<Person*> people;

for(auto it=people.begin();it!=people.end();it++){
 if((*it)->type==STUDENT_TYPE){
   //need to access student id number, classes etc. 
}
if((*it)->type==EMPLOYEE_TYPE){
  //need to access social insurance number

}

}

So my question is, how would one implement virtual functions in the "if statements", that allowed one to access data members (without dynamic_cast of course)

Thanks,

Mike

Advertisement

The more important question is why?

In this case, you should examine why you need to access two very different sets of data depending on the specific type of Person you're interacting with. Whatever that operation is should make sense for Person in general (and thus be a virtual member function of Person). If it does not make sense for Person, but only makes sense for the individual subtypes, then your object model is probably incorrect.

For example, a (potentially) acceptable operation would be "print the vital statistics of this person." This is a bit contrived, but also simple to demonstrate, so I'll use it. You'd then have a virtual void PrintVitals (std::ostream & stream); method. When implemented for Student, it would do something like stream << m_studentId << m_gpa << "\n". But for employee it might instead print the employee number and job title.

You would not want to make GetStudentID() a virtual member function of Person, because that creates an interface that is not actually valid for all people; not all people have student IDs.

Correctly designing OO interfaces is not about mimicking the real world exactly. It's about doing so where it makes sense, to ease and/or simplify the interface. In this case it very much sounds like (my first contrived example aside) you probably don't want to actually have a Person base class. You may want to check out the SOLID principles.

An alternative approach is to keep the Person base class, but dispose of the subclasses. Instead of inheritance, use composition. A person might have some common attributes like name and age, but student and employee data can be factored out and associated separately with the Person object.

Thanks Josh,

I like the composition idea, with the data separated out and still using the Person container. I will give it some thought and try to implement something clean.

Thanks,

Mike

Regarding SOLID, above, your code violates the LSP rule, so it's not a valid use of inheritance to begin with -- which is why you end up in "not recommended" situations :)

Put differently regarding the LSP rule, here is your problem:


for(auto it=people.begin();it!=people.end();it++){
if((*it)->type==STUDENT_TYPE){
//need to access student id number, classes etc.
}
if((*it)->type==EMPLOYEE_TYPE){
//need to access social insurance number
}

Instead, if you had followed the LSP, the could would be:

for(auto it=people.begin();it!=people.end();it++) {

(*it)->DoTheThing();

}

Users of the class should not need to know anything about the implementation details. If they are truly substitute objects then all of them have the same interface.

The class implements whatever variation there is within the object so that users of the class don't need to care.

While I fully agree with "good code has few or no casts" (ie if you need them, double check you're not doing something wrong), I am always suspicious when the biggest argument against some concept is performance.
As such I don't see any explanation why dynamic_cast is so wrong in the quotes of Scott Meyers.

If you don't use dynamic_cast, you reconsidered it, and reached the conclusion you cannot eliminate it, you must use something else, which is not free either.

The performance penalty of RTTI is unavoidable, and unlikely to be lower than that of virtual function calls (which are a less general mechanism).

Virtual functions are also a much cleaner, safer and more principled way to "unmix" objects of different types that have been confused together (like your students and employees).

Of course, in many context the best solution for your example is maintaining separate lists of students and employees rather than a single list of people.

Omae Wa Mou Shindeiru

Ideally you should structure your code so that always the derived class gets passed on, and from there you can cast to the base if necessary.

But there are times which, for multiple reasons, you need to downcast from base to derived; be it because of time constraints for good design, conflicting goals in the design or contradictory requirements, complex software, etc. And for some reasons virtual calls aren't a solution either.

In those exceptional cases, I usually do:


assert( dynamic_cast<Derived*>( basePtr ) );
Derived *myDerived = static_cast<Derived*>( basePtr );

This way my code will explode with an assert on debug mode if I somehow passed the wrong pointer; while still having zero overhead in release mode.

Though I usually pair it with something else, like:


assert( dynamic_cast<Derived*>( basePtr ) );
if( basePtr->getType() = DERIVED_TYPE )
     Derived *myDerived = static_cast<Derived*>( basePtr );

So that Release mode has very little overhead (a branch evaluating an int, which can have cache misses or misspredict) instead of the overhead of RTTI (which usually means string vs string comparison!)

The assert is still there just in case return value of getType() is incorrect.

A dynamic cast can be easily converted to a virtual function. For every "dynamic_cast<X*>", make a virtual function "X* GetX()", which returns "nullptr" in the base class, and "this" in the X class, eg (leaving out the declarations, constructors, etc).


class A {
    virtual B* GetB() { return nullptr; };
    virtual C* GetC() { return nullptr; };
};

class B : public A {
    virtual B* GetB() { return this; };
};

class C : public A {
    virtual C* GetC() { return this; };
};

Of course, the idea hasn't improved at all even if you code it in virtual functions.

For the above reason, I don't think dynamic_cast would be extremely expensive compared to virtual functions. A compiler could add this kind of code.

@Matias Goldberg


if( basePtr->getType() = DERIVED_TYPE )

That should be 2 '=' signs there :)

A dynamic cast can be easily converted to a virtual function. For every "dynamic_cast<X*>", make a virtual function "X* GetX()", which returns "nullptr" in the base class, and "this" in the X class, eg (leaving out the declarations, constructors, etc).

Ignoring whether or not this approach allows for efficient implementation of all the dynamic_cast rules (I'm not convinced it does), it's not how most compilers actually implement dynamic_cast. Most of the time they use the type information graph they've already generated and shoved somewhere for other type introspection purposes (like typeid and the like) which can't be implemented with runtime code (because they must work for compile time checks) and walk that in some fashion.
For example, cl.exe turns dynamic_cast operations into calls to __RTDynamicCast, which is this monstrosity:

extern "C" PVOID __CLRCALL_OR_CDECL __RTDynamicCast (
    PVOID inptr,            // Pointer to polymorphic object
    LONG VfDelta,           // Offset of vfptr in object
    PVOID SrcType,          // Static type of object pointed to by inptr
    PVOID TargetType,       // Desired result of cast
    BOOL isReference)       // TRUE if input is reference, FALSE if input is ptr
    throw(...)
{
    PVOID pResult=NULL;
    _RTTIBaseClassDescriptor *pBaseClass;


    if (inptr == NULL)
            return NULL;


    __try {


        PVOID pCompleteObject = FindCompleteObject((PVOID *)inptr);
        _RTTICompleteObjectLocator *pCompleteLocator =
            (_RTTICompleteObjectLocator *) ((*((void***)inptr))[-1]);
#if (defined(_M_X64)) && !defined(_M_CEE_PURE)
        unsigned __int64 _ImageBase;
        if (COL_SIGNATURE(*pCompleteLocator) == COL_SIG_REV0) {
            _ImageBase = GetImageBase((PVOID)pCompleteLocator);
        }
        else {
            _ImageBase = ((unsigned __int64)pCompleteLocator - (unsigned __int64)COL_SELF(*pCompleteLocator));
        }
#endif


        // Adjust by vfptr displacement, if any
        inptr = (PVOID *) ((char *)inptr - VfDelta);


        // Calculate offset of source object in complete object
        ptrdiff_t inptr_delta = (char *)inptr - (char *)pCompleteObject;


        if (!(CHD_ATTRIBUTES(*COL_PCHD(*pCompleteLocator)) & CHD_MULTINH)) {
            // if not multiple inheritance
            pBaseClass = FindSITargetTypeInstance(
                            pCompleteLocator,
                            (_RTTITypeDescriptor *) SrcType,
                            (_RTTITypeDescriptor *) TargetType
#if (defined(_M_X64)) && !defined(_M_CEE_PURE)
                            , _ImageBase
#endif
                            );
        }
        else if (!(CHD_ATTRIBUTES(*COL_PCHD(*pCompleteLocator)) & CHD_VIRTINH)) {
            // if multiple, but not virtual, inheritance
            pBaseClass = FindMITargetTypeInstance(
                            pCompleteObject,
                            pCompleteLocator,
                            (_RTTITypeDescriptor *) SrcType,
                            inptr_delta,
                            (_RTTITypeDescriptor *) TargetType
#if (defined(_M_X64)) && !defined(_M_CEE_PURE)
                            , _ImageBase
#endif
                            );
        }
        else {
            // if virtual inheritance
            pBaseClass = FindVITargetTypeInstance(
                            pCompleteObject,
                            pCompleteLocator,
                            (_RTTITypeDescriptor *) SrcType,
                            inptr_delta,
                            (_RTTITypeDescriptor *) TargetType
#if (defined(_M_X64)) && !defined(_M_CEE_PURE)
                            , _ImageBase
#endif
                            );
        }


        if (pBaseClass != NULL) {
            // Calculate ptr to result base class from pBaseClass->where
            pResult = ((char *) pCompleteObject) +
                      PMDtoOffset(pCompleteObject, BCD_WHERE(*pBaseClass));
        }
        else {
            pResult = NULL;
            if (isReference)
#ifndef _SYSCRT
                throw std::bad_cast("Bad dynamic_cast!");
#else
                throw bad_cast("Bad dynamic_cast!");
#endif
        }


    }
    __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION
              ? EXCEPTION_EXECUTE_HANDLER: EXCEPTION_CONTINUE_SEARCH)
    {
        pResult = NULL;
#ifndef _SYSCRT
        throw std::__non_rtti_object ("Access violation - no RTTI data!");
#else
        throw __non_rtti_object ("Access violation - no RTTI data!");
#endif
    }


    return pResult;
}

That's decidedly more expensive than a virtual table dispatch even before we bother to examine what the various Find*TypeInstance functions do.

This topic is closed to new replies.

Advertisement