RTTI and Casts

What is "RTTI"?

  • Run Time Type Information
  • RTTI is a mechanism that allows the type of an object to be determined during program execution (i.e. runtime).
  • It exposes information about an object’s data type at runtime and is available only for the classes which have at least one virtual function.

typeinfo and type_id

As we know, the mainly features of Oriented Object is encapsulation, inheritance and polymorphism.

For polymorphism, a pointer of base class can point to a sub-class object instance, i.e. Base *ptr = new Derived().

So, in such case, how can we know the real type of an object that ptr points to? That is why there is typeid in C++.

Here is some introductions, copied from Microsoft Docs.

The typeid operator allows the type of an object to be determined at run time.

The result of typeid is a const type_info&. The value is a reference to a type_info object that represents either the type-id or the type of the expression, depending on which form of typeid is used. For more information, see type_info Class.

The typeid operator does a run-time check when applied to an l-value of a polymorphic class type, where the true type of the object can't be determined by the static information provided. Such cases are:

  • A reference to a class
  • A pointer, dereferenced with *
  • A subscripted pointer ([ ]). (It's not safe to use a subscript with a pointer to a polymorphic type.)

And please note that typeid(expr), where expr must be a class with virtual functions.

  • If the expression points to a base class type, yet the object is actually of a type derived from that base class, a type_info reference for the derived class is the result.
  • The expression must point to a polymorphic type (a class with virtual functions). Otherwise, the result is the type_info for the static class referred to in the expression. (We will explain this by an example.)
  • Further, the pointer must be dereferenced so that the object used is the one it points to. Without dereferencing the pointer, the result will be the type_info for the pointer, not what it points to.

Let us see an example.

#include <iostream>
#include <typeinfo>

class Base {
public:
    virtual void vvfunc() {}
};

class Derived : public Base {};

using namespace std;
int main() {
    Derived *pd = new Derived;
    Base *pb = pd;
    cout << typeid(pb).name() << endl;  // prints "P4Base"
    cout << typeid(*pb).name() << endl; // prints "7Derived"
    cout << typeid(pd).name() << endl;  // prints "P7Derived"
    cout << typeid(*pd).name() << endl; // prints "7Derived"
    delete pd;
}

Here, P means "pointer", and the number 7 or 4 means the length of class name "Derived", "Base".

Let us see another example, where the base class has no virtual function.

#include <cassert>
#include <iostream>
#include <typeinfo>
using namespace std;
class A {
private:
    int a;
};

class B : public A {
public:
    virtual void f() {}
};

class C : public B {
public:
    virtual void f() {}
};
int main() {
    A *pa = new B();
    B *pb = new C();

    assert(strcmp(typeid(*pa).name(), "1A") == 0);
    assert(strcmp(typeid(*pb).name(), "1C") == 0);
}

Since A is not a polymorphism type, typeid(*pa) points to the type_info object representing class A.

What's more, we can perform typeid on static type. The static type of an expression refers to the type of an expression as it is known at compile time.

#include <typeinfo>
int main()
{
    typeid(int) == typeid(int&); // evaluates to true
}

typeid can also be used in templates to determine the type of a template parameter:

#include <typeinfo>
template <typename T>
T max(T arg1, T arg2) {
    cout << typeid(T).name() << "s compared." << endl;
    return ( arg1 > arg2 ? arg1 : arg2 );
}

dynamic_cast

The dynamic_cast operator is used to converts the operand expression to an object of type type-id.

dynamic_cast < type-id > ( expression )
  • The type-id must be a pointer or a reference to a previously defined class type or a "pointer to void".
  • The type of expression must be a pointer if type-id is a pointer, or an l-value if type-id is a reference.

As we know, we can cast one type pointer to another type in C language (we can do anything we want, almostly).

In C++, dynamic_cast will help us do some safety checks. If the casting is unsafe, then dynamic_cast will throw a bad_cast Exception (or return a nullptr in some cases). For example,

#include <iostream> // std::cout
#include <typeinfo> // std::bad_cast

class Base { virtual void member() {} };
class Derived : Base {};

int main() {
    try {
        Base b;
        Derived &rd = dynamic_cast<Derived &>(b);
    } catch (std::bad_cast &bc) {
        std::cerr << "bad_cast caught: " << bc.what() << '\n';
    }
    return 0;
}

This program will output:

bad_cast caught: std::bad_cast

Upcast

class B {
    virtual void f() {}
};
class C : public B {};
class D : public C {};

void f(D *pd) {
    C *pc = dynamic_cast<C *>(pd); // ok: C is a direct base class
                                   // pc points to C subobject of pd
    B *pb = dynamic_cast<B *>(pd); // ok: B is an indirect base class
                                   // pb points to B subobject of pd
}

int main() { f(new D()); }

This type of conversion is called an "upcast" because it moves a pointer up a class hierarchy, from a derived class to a class it is derived from. An upcast is an implicit conversion.

Downcast

class B {
    virtual void f() {}
};
class D : public B {
    virtual void f() {}
};

void f() {
    B *pb = new D(); // unclear but ok
    B *pb2 = new B();

    D *pd = dynamic_cast<D *>(pb);   // ok: pb actually points to a D, downcast
    D *pd2 = dynamic_cast<D *>(pb2); // pd2 is nullptr
}

int main() { f(); }

This type of conversion is called a "downcast" because it moves a pointer down a class hierarchy, from a given class to a class derived from it.

If the type of expression is a base class of the type of type-id, a run-time check is made to see if expression actually points to a complete object of the type of type-id. If this is true, the result is a pointer to a complete object of the type of type-id.

Please note that using a derived class pointer points to a base class object instance, such behavior is unsafe. i.e.

D *pd1 = dynamic_cast<D *>(new B()); // ok, pd1 is nullptr, it's safe
D *pd2 = (D *)(new B());             // ok, pd2 != nullptr, but it's unsafe

Such statements can be complied successfully, but they are not good code.

Multiple Inheritance

Consider we have such inheritance case:

  A
 / \
B   C
 \ /
  D
  • A pointer to an object of type D can be safely cast to B or C.
  • If D is casted to point to an A object, this would result in an ambiguous casting error.

Let me explain the second one by an example.

class A {
    virtual void f();
};
class B : public A {
    virtual void f();
};
class C : public A {
    virtual void f();
};
class D : public B, public C {
    virtual void f();
};

void f() {
    D *pd = new D;
    A *pa = dynamic_cast<A *>(pd);  // Compiler error: ambiguous conversion from
                                    // derived class 'D' to base class 'A'
    B *pb = dynamic_cast<B *>(pd);  // ok, cast to B
    C *pc = dynamic_cast<C *>(pd);  // ok, cast to C

    A *pa2 = dynamic_cast<A *>(pb); // ok, and it's unambiguous
}

Since there are two inheritance chains, the compiler do not know which one should be choosed. Complied it with clang++ -std=c++17, it will output:

error: ambiguous conversion from derived class 'D' to base class 'A':
    class D -> class B -> class A
    class D -> class C -> class A
    A *pa = dynamic_cast<A *>(pd);  // Compiler error: ambiguous conversion from

Cross-cast

Consider such an inheritance case:

  A
 / \
B   C   D
 \  |  /
    E

The dynamic_cast operator can also be used to perform a "cross cast". Using the same class hierarchy, it is possible to cast a pointer, for example, from the B subobject to the D subobject, as long as the complete object is of type E.

class A {
    virtual void f() {}
};

class B : public A {
    virtual void f() {}
};

class C : public A {};

class D {
    virtual void f() {}
};
class E : public B, public C, public D {
    virtual void f() {}
};

void f(D *pd) {
    B *pb = dynamic_cast<B *>(pd); // cross cast
    A *pa = pb;                    // upcast, implicit conversion
}

int main() { f(new E()); }

Summary

Actually, an pointer is a 32-bit or 64-bit integer. In C language, we usually forcedly cast one type to another type, but we should know what we are doing exactly. Otherwise, the incompatible casting will cause system crashes.

C++ is an oriented object programming language, hence we should do some safety checks when performing an running time type casting. This is what dynamic_cast help us do.

Here is an example, showing the most common usages of dynamic_cast.

#include <iostream>
#include <stdio.h>

struct A {
    virtual void test() { printf("in A\n"); }
};

struct B : A {
    virtual void test() { printf("in B\n"); }

    void test2() { printf("test2 in B\n"); }
};

struct C : B {
    virtual void test() { printf("in C\n"); }

    void test2() { printf("test2 in C\n"); }
};

void Globaltest(A &a) {
    try {
        C &c = dynamic_cast<C &>(a);
        printf("in GlobalTest\n");
    } catch (std::bad_cast) {
        printf("Can't cast to C\n");
    }
}

int main() {
    A *pa = new C;
    A *pa2 = new B;

    pa->test();  // in C
    pa2->test(); // in B

    B *pb = dynamic_cast<B *>(pa);
    if (pb)
        pb->test2(); // test2 in B

    /* pc is nullptr, since the object B knows nothing about C,
     * even if this object is pointed by `A*`.
     */
    C *pc = dynamic_cast<C *>(pa2);
    assert(pc == nullptr);

    C ConStack;
    Globaltest(ConStack); // in GlobalTest

    /* will fail because B knows nothing about C */
    B BonStack;
    Globaltest(BonStack); // Can't cast to C
}

Other casts

By the way, let us take a look on the other cast operators: static_cast, const_cast, reinterpret_cast.

const_cast

The const_cast is used to remove the const, volatile, and __unaligned attribute(s) from a class.

An naive usage is:

int main() {
    const int *p = new int[1];
    *p = 123; // compiler error: read-only variable is not assignable
    *const_cast<int *>(p) = 123; // ok
    cout << *p << endl;
    delete[] p;
}

In class, it could remove the const qualifier after the function(s).

#include <iostream>
using namespace std;
class CCTest {
public:
    void setNumber(int num) { number = num; }
    void printNumber() const {
        cout << "Before: " << number << "\n";
        const_cast<CCTest *>(this)->number--;
        cout << "After: " << number << "\n";
    }
private:
    int number;
};

int main() {
    CCTest X;
    X.setNumber(8);
    X.printNumber();
}

static_cast

static_cast converts an expression to the type of type-id, based only on the types that are present in the expression.

static_cast <type-id> ( expression )
  • static_cast does no run-time type check, while dynamic_cast does.
  • A dynamic_cast to an ambiguous pointer will fail, while a static_cast returns as if nothing were wrong; this can be dangerous.

Example

#include <iostream>
using namespace std;
class B {
    virtual void f() {}
};

class D : public B {
  public:
    int val = 123;
    void f2() { printf("D::f2(), val = %d\n", val); }
};

void f(B *pb, D *pd) {
    D *pd2 = static_cast<D *>(pb); // Not safe, D can have fields
                                   // and methods that are not in B.

    pd2->f2();      // val is an uncertain value
    pd2->val = 233; // this may damage other data

    B *pb2 = static_cast<B *>(pd); // Safe conversion, D always
                                   // contains all of B.
}

int main() {
    B b;
    D d;
    f(&b, &d);
}

If we let D *pd2 = dynamic_cast<D *>(pb), then both pd2->f2() and pd2->val = 233 will cause segment fault, since dynamic_cast will do some safety checks, and pd2 is an null pointer in such case.

reinterpret_cast

reinterpret allows any pointer to be converted into any other pointer type. Also allows any integral type to be converted into any pointer type and vice versa.

However, the reinterpret_cast operator can not cast away the const, volatile, or __unaligned attributes.

It is equivalent to forced type cast in C language.

One practical use of reinterpret_cast is in a hash function, which maps a value to an index in such a way that two distinct values rarely end up with the same index.

#include <iostream>
using namespace std;

// Returns a hash code based on an address
unsigned short Hash(void *p) {
    unsigned int val = reinterpret_cast<unsigned int>(p);
    return (unsigned short)(val ^ (val >> 16));
}

using namespace std;
int main() {
    int a[20];
    for (int i = 0; i < 20; i++)
        cout << Hash(a + i) << endl;
}

Summary

  • dynamic_cast - Used for conversion of polymorphic types.
  • static_cast - Used for conversion of non-polymorphic types.
  • const_cast - Used to remove the const, volatile, and __unaligned qualifiers.
  • reinterpret_cast - Used for simple reinterpretation of bits, which is equivalent to forced type cast in C language.

References

posted @ 2022-02-12 17:27  sinkinben  阅读(218)  评论(0编辑  收藏  举报