Detailed explanation of RTTI mechanism in C++

  • 2020-04-02 02:49:20
  • OfStack

preface

RTTI is short for Runtime Type Information, which means Runtime Type Information, and it provides a method for determining the Type of an object at Runtime. RTTI is not new. The technology has been around for a long time, but it has been used sparingly in practice. And I'm summarizing RTTI here, and just because I didn't use it today doesn't mean it's not useful. Let's start with the typeid function.

Typeid function

The main purpose of a typeid is to let the user know what type the current variable is, such as the following:


#include <iostream>
#include <typeinfo>
using namespace std;
 
int main()
{
     short s = 2;
     unsigned ui = 10;
     int i = 10;
     char ch = 'a';
     wchar_t wch = L'b';
     float f = 1.0f;
     double d = 2;
 
     cout<<typeid(s).name()<<endl; // short
     cout<<typeid(ui).name()<<endl; // unsigned int
     cout<<typeid(i).name()<<endl; // int
     cout<<typeid(ch).name()<<endl; // char
     cout<<typeid(wch).name()<<endl; // wchar_t
     cout<<typeid(f).name()<<endl; // float
     cout<<typeid(d).name()<<endl; // double
 
     return 0;
}

For built-in types supported by C++, the typeid is fully supported, and by calling the typeid function, we can know the information of the variable. What about our custom structs, classes?


#include <iostream>
#include <typeinfo>
using namespace std;
 
class A
{
public:
     void Print() { cout<<"This is class A."<<endl; }
};
 
class B : public A
{
public:
     void Print() { cout<<"This is class B."<<endl; }
};
 
struct C
{
     void Print() { cout<<"This is struct C."<<endl; }
};
 
int main()
{
     A *pA1 = new A();
     A a2;
 
     cout<<typeid(pA1).name()<<endl; // class A *
     cout<<typeid(a2).name()<<endl; // class A
 
     B *pB1 = new B();
     cout<<typeid(pB1).name()<<endl; // class B *
 
     C *pC1 = new C();
     C c2;
 
     cout<<typeid(pC1).name()<<endl; // struct C *
     cout<<typeid(c2).name()<<endl; // struct C
 
     return 0;
}

Yes, tpyeid supports both our custom structs and classes. In the above code, after the typeid is called, the name() function is called, and you can see that the typeid function returns a struct or class. In fact, a typeid is a function that returns a type_info type. So it's worth summarizing the type_info class, which actually holds the type information.

Type_info class

Remove the damn macros and view the type_info class definition in Visual Studio 2012 as follows:


class type_info
{
public:
    virtual ~type_info();
    bool operator==(const type_info& _Rhs) const; //Used to compare whether two objects have the same type
    bool operator!=(const type_info& _Rhs) const; //Used to compare whether the types of two objects are different
    bool before(const type_info& _Rhs) const;
 
    //Returns the type name of the object, which is used a lot with
    const char* name(__type_info_node* __ptype_info_node = &__type_info_root_node) const;
    const char* raw_name() const;
private:
    void *_M_data;
    char _M_d_name[1];
    type_info(const type_info& _Rhs);
    type_info& operator=(const type_info& _Rhs);
    static const char * _Name_base(const type_info *,__type_info_node* __ptype_info_node);
    static void _Type_info_dtor(type_info *);
};

In the type_info class, both the copy constructor and the assignment operator are private, and there is no default constructor. Therefore, there is no way to create A variable of the type_info class, such as type_info A; This is wrong. So how does the typeid function return a reference to an object of the type_info class? I'm not going to talk about it here, but the idea is the friend function of the class.

Use of the typeid function

Typeid is very simple to use and is commonly used in the following two ways:

Use the name() function in the type_info class to return the type name of the object

As used in the above code; However, there is one caveat, such as the following code:


#include <iostream>
#include <typeinfo>
using namespace std;
 
class A
{
public:
     void Print() { cout<<"This is class A."<<endl; }
};
 
class B : public A
{
public:
     void Print() { cout<<"This is class B."<<endl; }
};
 
int main()
{
     A *pA = new B();
     cout<<typeid(pA).name()<<endl; // class A *
     cout<<typeid(*pA).name()<<endl; // class A
     return 0;
}

I used typeid twice, but the parameters are different; The output is different; When I specify pA, since pA is A pointer of type A, the output is class A *; When I specify *pA, it represents the type of object that pA points to, so it prints class A; So you need to distinguish between typeid(*pA) and typeid(pA), they're not the same thing; But here's the problem, why do you get class A when pA is actually pointing to B? Let's look at the next piece of code:


#include <iostream>
#include <typeinfo>
using namespace std;
 
class A
{
public:
     virtual void Print() { cout<<"This is class A."<<endl; }
};
 
class B : public A
{
public:
     void Print() { cout<<"This is class B."<<endl; }
};
 
int main()
{
     A *pA = new B();
     cout<<typeid(pA).name()<<endl; // class A *
     cout<<typeid(*pA).name()<<endl; // class B
     return 0;
}

Okay, so I made the Print function virtual, and the output is different, so what does that mean? This is what RTTI is up to. When there are no virtual functions in the class, the typeid is compile-time, that is, statically typed, as shown in cout < < Typeid (* pA). The name () < < Endl; Output class A the same; When a virtual function exists in a class, the typeid is a run-time thing, that is, a dynamic type, like cout above < < Typeid (* pA). The name () < < Endl; The output class B is the same, and this is one of the things that we're going to make a lot of mistakes in actual programming, so keep that in mind.

2. Use the overloaded == and! In the type_info class. = compare the types of two objects

This is often used to compare objects of two classes with virtual functions, such as the following code:


#include <iostream>
#include <typeinfo>
using namespace std;
 
class A
{
public:
     virtual void Print() { cout<<"This is class A."<<endl; }
};
 
class B : public A
{
public:
     void Print() { cout<<"This is class B."<<endl; }
};
 
class C : public A
{
public:
     void Print() { cout<<"This is class C."<<endl; }
};
 
void Handle(A *a)
{
     if (typeid(*a) == typeid(A))
     {
          cout<<"I am a A truly."<<endl;
     }
     else if (typeid(*a) == typeid(B))
     {
          cout<<"I am a B truly."<<endl;
     }
     else if (typeid(*a) == typeid(C))
     {
          cout<<"I am a C truly."<<endl;
     }
     else
     {
          cout<<"I am alone."<<endl;
     }
}
 
int main()
{
     A *pA = new B();
     Handle(pA);
     delete pA;
     pA = new C();
     Handle(pA);
     return 0;
}

This is one usage, and I'll summarize later how to use dynamic_cast to do the same.

Dynamic_cast insider

The article "link: #" also introduces the use of dynamic_cast. There is no explanation of how dynamic_cast is actually implemented, but here is the inside story of dynamic_cast. Let's start with a piece of code:


#include <iostream>
#include <typeinfo>
using namespace std;
 
class A
{
public:
     virtual void Print() { cout<<"This is class A."<<endl; }
};
 
class B
{
public:
     virtual void Print() { cout<<"This is class B."<<endl; }
};
 
class C : public A, public B
{
public:
     void Print() { cout<<"This is class C."<<endl; }
};
 
int main()
{
     A *pA = new C;
     //C *pC = pA; // Wrong
     C *pC = dynamic_cast<C *>(pA);
     if (pC != NULL)
     {
          pC->Print();
     }
     delete pA;
}

In the above code, if we assign pA directly to pC, the compiler will give us an error, and when we add dynamic_cast, everything will be ok. So what does dynamic_cast do behind it?

Dynamic_cast is primarily used in polymorphic situations and allows type conversions at run time, enabling programs to safely convert types in a class hierarchy, converting base class Pointers (references) to derived class Pointers (references). As I summarized in the post "link: #", when virtual functions exist in a class, the compiler adds a VPTR pointer to the virtual function table in the member variable of the class. The associated type_info object of each class is also indicated by the virtual table, usually the type_info object is placed in the first slot of the table. When we do dynamic_cast, the compiler will check the syntax for us. If the static type of the pointer is the same as the target type, then nothing is done. Otherwise, the pointer is first adjusted to point to the vftable and passed to the inner function along with the adjusted pointer, the adjusted offset, the static type, and the target type. The last parameter indicates whether the transformation is a pointer or a reference. The only difference between the two is that the former returns NULL if the conversion fails and the latter throws a bad_cast exception. For the example program in the use of the typeid function, I used dynamic_cast to make the change as follows:


#include <iostream>
#include <typeinfo>
using namespace std;
 
class A
{
public:
     virtual void Print() { cout<<"This is class A."<<endl; }
};
 
class B : public A
{
public:
     void Print() { cout<<"This is class B."<<endl; }
};
 
class C : public A
{
public:
     void Print() { cout<<"This is class C."<<endl; }
};
 
void Handle(A *a)
{
     if (dynamic_cast<B*>(a))
     {
          cout<<"I am a B truly."<<endl;
     }
     else if (dynamic_cast<C*>(a))
     {
          cout<<"I am a C truly."<<endl;
     }
     else
     {
          cout<<"I am alone."<<endl;
     }
}
 
int main()
{
     A *pA = new B();
     Handle(pA);
     delete pA;
     pA = new C();
     Handle(pA);
     return 0;
}

This is a rewritten version using dynamic_cast. In real projects, this approach is used more often.

conclusion

Here I summarized the knowledge of RTTI, I hope you understand. This blog post is a bit long, I hope you have patience to read it. Summed up will have the harvest.


Related articles: