{ Notes }


A brief overview of C++ Type conversion and RTTI

Posted on | C++


Type casting or Type Conversion is a process of converting a value of one data type to another data type. In C++, type casting takes place implicitly and explicitly, implicit type casting is done automatically by the compiler while explicit type casting is undertaken by programmer. There have always been confusion around C++ explicit type casting, particularly when dealing with polymorphic pointers and references. In this post I am trying to explain uses of C++ specific type conversion functions when dealing with user defined classes.

Coercion

the action or practice of persuading someone to do something by using force or threats.

Implicit Type conversion (Coercion)

Briefly speaking implicit conversion is done automatically by the compiler, which includes type promotions or conversions. Type promotion is when compiler promotes a lower type value to a higher type, such as when assigning double dd(0.010f), float value will automatically become 0.010 as double type. Moreover double dd = 3 will convert int type to double as 3.0, this is done by compiler itself by converting one type to a required type.

Explicit Type conversion

This type conversion is undertaken by programmer, by doing this programmer is determined by the type conversions being done, in fact he or she takes complete responsibility for casting. Mainly explicit casting is done when dealing with polymorphic objects and classes with inheritance relationship.

C Style

Conventionally C styled casts are well known amongst programmers such that converting from int to long or long to int long b = 100000; int a = (int) b; basically these type conversions are used on fundamental data types correspondingly conversion is done at compile time, therefore they are fundamentally safe.

Basically conventional style casts are rudimentary, therefore pose limitations when dealing with class hierarchies and strong type checking. Fortunately C++ provides operators to perform strong type checking on pointers and references types for polymorphic and non polymorphic classes utilised for upcasting from derived->base or down-casting from base->derived. Unlike conventional casts, C++ casting operators provides strict type checking and restricted casting, it ensures that not any pointer or reference type could be converted to any pointer type it must be complete object.

Accordingly, I will be using two classes as basis for explaining C++ casting operators, class Shape is a base class defined with a virtual function area and class Other is derived from base and overrides virtual function area.

class Shape {
    int R_;
public:
    Shape(){
        std::cout << "Base::Shape() " << std::endl;
    }
    virtual void area(){
        std::cout << "base::area " << std::endl;
    }
};

class Other : public Shape {
public:
    Other(){
        // std::cout << "Other" << std::endl;
    }
    void area(){
        std::cout << "Other::area " << std::endl;
    }
};

Const_cast

const_cast<>() operator is intended for conversion of non-constant or non-volatile pointers to constant or volatile types and vice-versa. Ideally const_cast can be used when passing a non-constant pointer to a function accepting const types as arguments, further more while calling a non-constant function through a constant pointer. For instance, a constant instance of class Other will generate compilation error.

const Other* ot = new Other;
ot->area();
passing 'const Other' as 'this' argument of 'virtual void Other::area()' discards qualifiers [-fpermissive]
  ot->area();

Therefore simply replacing ot->area() with const_cast<Other*>(ot)->area(); resolves the issue, essentially const_cast simply converts a constant pointer to non-constant type to invoke non-constant function, similarly this conversion technique can be used for volatile and non-volatile pointers.

Reinterpret_cast

reinterpret_cast<>() is unportable and unsafe casting mechanism of C++, mainly because it simply cast pointers from one type to another irrespective of any relationship they posses, it simply converts pointers without type checking. Basically it creates a binary copy from one type to another. Generally reinterpret_cast is not recommended for use in your code however it is still available in case there is a need for it. The most common pointer conversion use of reinterpret_cast is while converting from void to known object type, such as converting a void allocated memory to an array of type int. int* arr = reinterpret_cast<int*>(voidArr*);

Static_cast

static_cast<>() can be used against C style cast but notably it allows conversion only on pointer or reference types related with inheritance relationship or between polymorphic types. Typically upcasting and down-casting of pointer types is possible with static_cast although but no assurance of safe typecast due to lack of type checking occurring at runtime. Type checking occurs at compile time therefore if used at runtime, then it is programmer's responsibility to be aware of typecasting between pointers or references being done. Although static_cast is less expensive because it does not performs any type checking at runtime, preferably static_cast is recommended over dynamic_cast if casting between two pointer types are known by programmer.

Other* d = new Other;
Shape* b = new Shape;
Shape* bd = static_cast<Shape*>(d);//upcasting from derived->base
Other* db = static_cast<Other*>(b);//down-casting from base->derived
Polymorphic Class
A class containing atleast one virtual method

Dynamic_cast

Dynamic_cast<>() only allows casting between pointer and reference types of classes of polymorphic types and with inheritance relationship. Typically this casting operator can be used for upcasting and down-casting on pointer or reference types at runtime, unlike static casting which performs type checking at compile time. Considering code block above, when upcasting from pointer of class Shape to class Other, upcasting from pointer of type Other to type Base will be successful as both types are polymorphic, the type conversion will ensure that the type to be converted is a complete type.

Other* d = new Other;
Shape* b = new Shape;
Shape* bd = dynamic_cast<Shape*>(d);
std::cout << "Pointer bd " << (long)bd << std::endl;

Moreover if two classes are not polymorphic, the result of pointer conversion is null. For instance in code block below an additional virtual class Square is used but it is not related with inheritance relationship consequently casting with dynamic_cast will return null or zero. Moreover dynamic casting ensures that casting type is a complete object of destination object for this it performs a complete type check on both objects and this all process occurs at runtime involving virtual table reading consequently this process is complex and expensive.

class Square {
    int X_;
    int Y_;
public:
    Square(){}
    virtual void area(){}
};
///inside main function
Square* s = new Square;
bd = dynamic_cast<Shape*>(s);
std::cout << "Pointer bd " << (long)bd << std::endl;

RunTime Type Identification (RTTI)

RTTI is a mechanism of exposing information about an object type at runtime, although RTTI is just technique, therefore to obtain information about an object dynamic_cast operator along with typeid keyword are typically used, moreover additional function name() provided by typeid operator can be used to get string id of an object, they are used by including <typeinfo> header file.

Further typeid(<name_of_pointer_object>).name() returns string representation of class id. Ideally the argument supplied is a pointer object then it return a class id string preffixed with p meaning pointer type and if argument supplied is a object it self then the string will be without prefix.

Other* d = new Other;
Shape* b = new Shape;
Shape* bd = dynamic_cast<Shape*>(d);
std::cout << "D " << typeid(*d).name() << std::endl;//D 5Other
std::cout << "BD " << typeid(bd).name() << std::endl;//BD P5Shape

Furthermore while casting two unrelated polymorphic classes using dynamic cast, ideally one would check if cast operation does not return zero. However when downcasting from base to derived it is best to check if base type is a complete type of destination type, therefore if they relate then downcast becomes safe also avoid expensive operation performed by dynamic_cast. Accordingly a conditional check of typeid on the two pointer types help to perform a safe type conversion, considering code block above if object bd is type casted to Other type it is best to check if they are related, since bd is created from derived type d therefore casting to another Other object from it should be fine, see code below.

Other* o;
if(typeid(*d) == typeid(*bd))
    o = dynamic_cast<Other*>(bd);

Eventually on concluding notes, static_cast operator is less expensive and performs type checking at compile time however it does not guarantee type conversion. While dynamic_cast performs guaranted type checking at runtime but it is expensive to use, moreover both casting operators only allow type checking on polymorphic and classes related with inheritance. Evindently if casting between primitive data types its best to use old C style casting however for guarantee on primitive types its best to use static_cast<> over old C style casting.