Four types of conversions in C++

  • 2020-04-02 03:17:26
  • OfStack

1 the introduction

This note is based on a question on StackOverflow. The main content is to illustrate four types of C/C++ conversion operations. In fact, I had some understanding of them before, but as I summarized them and typed some test sample code for verification, I had a deeper understanding of them.

As you know, four new type conversion operators have been introduced in C++ : static_cast, dynamic_cast, reinterpret_cast, and const_cast. In some of the C++ code I've seen, their use is not common. Many programmers still prefer to use c-like casts because they are powerful and easy to write. Can you believe that the c-like cast operator actually includes three operators, static_cast, const_cast, and reinterpret_cast? Let's see.

Note: you will be familiar with the two forms of c-like type conversion operations mentioned above.


(new-type) expression
new-type (expression)

2 static_cast vs dynamic_cast

The static_cast and dynamic_cast brothers are put together because they are easier to remember than the two. First of all, from the name above they have semantic relative relation, one "static" and one "dynamic". In addition, the features of this comparison are also reflected to some extent in the functions, such as the run-time Checkingt of dynamic_cast and the type detection added by static_cast at compile time. In short:

Static_cast: 1) completes the basic data type, 2) converts the type in the same inheritance system
Dynamic_cast: using the polymorphic scenario, adds a layer of checks on the actual calling object type

2.1 from c-like to static_cast

Static_cast handles base types such as int, float, char, and their corresponding Pointers mostly Like c-like conversions, but static_cast is more secure.


char c = 10;      //1 byte
int *p = (int *)&c;  //4 bytes (32bit platform)

*p = 5;        //Memory on the dirty
int *q = static_cast<int *>(&c); //This error can be checked out at compile time using static_cast.


It also provides an additional layer of protection over c-like for custom type processing, that is, it does not support conversion between types that are not part of the same inheritance scheme. But c-like can do it. Here's an example:


#include <iostream>

class A
{
public:
 A(){}
 ~A(){}
 
private:
 int i, j;
};

class C
{
public:
 C(){}
 ~C(){}

 void printC()
 {
  std::cout <<"call printC() in class C" <<std::endl;
 }
private:
 char c1, c2;
};

int main()
{ 
 A *ptrA = new A;
 //C *ptrC = static_cast<C *>(ptrA);
 //Compilation failed, prompt:
 // In function  ' int main()':
 // error: invalid static_cast from type  ' A*' to type  ' C*'
 
 C *ptrC = (C *)(ptrA);
 ptrC->printC();
 //The compilation passed normally.
 //Although printC can be called normally at this point, the result is actually "undefined"
 //I have tried, if I add some operation of data member, it will make the result of operation unpredictable
 //Therefore, at run time this logically related behavior is not clear.
 
 return 0;
} 

2.2 static_cast for conversion of custom types

The small example above simply compares the differences in static_cast and c-like performance between classes for different inheritance systems, but now narrow down to type conversions within the same inheritance system. (note: the type in question is usually a pointer to a class or a reference to a class.)

Static_cast is for transformations between classes of the same inheritance architecture, which can do both upcast and downcast. Generally speaking, there is no problem when doing upcast. After all, there must be a set of related operations of the parent class in the subclass, so it is safe to manipulate the corresponding object through the converted pointer or reference. This is the same as using the static_cast as using c-like or direct implicit conversion (of course, whether the result matches the programmer's own expectations depends on the design at the time).

Note that downcast with static_cast should be avoided, as it can escape the compiler's attention, but can cause undefined problems at run time:


#include <iostream>

class A
{
public:
 A():i(1), j(1){}
 ~A(){}
 
 void printA()
 {
  std::cout <<"call printA() in class A" <<std::endl;
 }
 
 void printSum()
 {
  std::cout <<"sum = " <<i+j <<std::endl;
 }
 
private:
 int i, j;
};

class B : public A
{
public:
 B():a(2), b(2) {}
 ~B(){}

 void printB()
 {
  std::cout <<"call printB() in class B" <<std::endl;
 }
 
 void printSum()
 {
  std::cout <<"sum = " <<a+b <<std::endl;
 }
 
 void Add()
 {
  a++;
  b++;
 }
 
private:
 double a, b;
};

int main()
{   
 B *ptrB = new B;
 ptrB->printSum();
 //Print result: sum = 4
 A *ptrA = static_cast<B *>(ptrB);  
 ptrA->printA();
 ptrA->printSum();
 //Print result: sum = 2
 //When doing an upcast, the behavior of the object to which the pointer is pointing depends on the type of pointer.
 
 
 ptrA = new A;
 ptrA->printSum();
 //Print result: sum = 2 
 ptrB = static_cast<B *>(ptrA);
 ptrB->printB();
 ptrB->printSum(); 
 //Print result: sum = 0
 //When you do a downcast, its behavior is "undefined."
 
 //B b;
 //B &rB = b;
 //rB.printSum();
 //Print result: sum = 4
 //A &rA = static_cast<A &>(rB);  
 //rA.printA();
 //rA.printSum();
 //Print result: sum = 2
 //When doing an upcast, the behavior of the object to which the reference refers depends on the type of reference.
 
 //A a;
 //A &rA = a;
 //rA.printSum();
 //Print result: sum = 4
 //B &rB = static_cast<B &>(rA);  
 //rB.printB();
 //rB.printSum();
 //Print result: sum = 5.18629e-317
 //When you do a downcast, its behavior is "undefined."
 
 return 0;
}

As shown above, the performance of static_cast when downcast between classes belonging to the same inheritance architecture will be undefined, as will the performance of c-like when converted between classes belonging to different inheritance architectures. Therefore, whenever possible, downcast transformations should be performed using static_cast and, more precisely, downcast transformations of Pointers or references to the classes of the integration architecture should be avoided whenever possible.

In that case, doesn't downcast exist in the software development process? Not really. Typically, a downcast is performed in a virtual inheritance scenario, where dynamic_cast is used.

2.3 dynamic_cast

Dynamic_cast is mainly used in the scenario of downcast, and its use needs to meet two conditions:

There is a "virtual inheritance" relationship between the transformed classes at downcast time
The converted type should match the actual type it points to
Dynamic_cast works the same for upcast as for static_cast, but because dynamic_cast relies on RTTI, it is slightly lower in performance than static_cast.


#include <iostream>
#include <exception>

class A
{
public:
 virtual void print() 
 {
  std::cout <<"Welcome to WorldA!" <<std::endl;
 }
};

class B : public A
{
public:
 B():a(0), b(0) {}
 ~B(){}
 virtual void print() 
 {
  std::cout <<"Welcome to WorldB!" <<std::endl;
 }
private:
 double a, b;
};

int main()
{
 B *ptrB = new B;
 A *ptrA = dynamic_cast<A *>(ptrB);
 ptrA->print();
 //In virtual inheritance, the dynamic_cast transform works the same as the static_cast when an upcast is performed against a pointer
 //There is no requirement for the existence of a virtual object, and the member of the pointed object is actually called.
  
 //A *ptrA = new A;
 //B *ptrB = dynamic_cast<B *>(ptrA);
 //ptrB->print();
 //Segmentation fault, when downcast is performed against a pointer, the conversion fails and NULL is returned.
 
 //A a;
 //A &ra = a;
 //B &b = dynamic_cast<B &>(ra);
 //b.print();  
 //Throws a St8bad_cast exception, which is thrown when the downcast was not successful against the reference.
 
 //ptrA = new A;
 //ptrB = static_cast<B *>(ptrA);
 //ptrB->print();
 //When using static_cast for downcast, instead of dynamic_cast returning NULL,
 //This will call the virtual function of the object that the ptrB actually points to.
  
 //ptrA = new A;
 //ptrB = dynamic_cast<B *>(ptrA);
 //ptrB->print();
 //When you do downcast, if there are no virtual members, you will be prompted at compile time:
 // In function  ' int main()':
 // cannot dynamic_cast  ' ptrA' (of type  ' class A*') to type  ' class B*' (source type is not polymorphic)
 
 return 0;
}

As you can see from this example, static_cast must be used where dynamic_cast can be used in a virtual inheritance scenario, but dynamic_cast has more stringent requirements to help programmers write more rigorous code. However, it has a bit more overhead in terms of performance.

3 reinterpret_cast

Reinterpret_cast is the most dangerous kind of cast, and the reason it's the most dangerous is because it's as strong as c-like and can go wrong with the slightest bit of carelessness. It is commonly used in low-level transformations or bit operations.


#include <iostream>

class A
{
public:
 A(){}
 ~A(){}
 void print() 
 {
  std::cout <<"Hello World!" <<std::endl;
 }
};

class B
{
public:
 B():a(0), b(0) {}
 ~B(){}

 void call()
 {
  std::cout <<"Happy for your call!" <<std::endl;
 }

private:
 double a, b;
};

int main()
{
 //A *ptrA = new A;
 //B *ptrB = reinterpret_cast<B *>(ptrA);
 //ptrB->call();
 //Normal compilation
 //A *ptrA = new A;
 //B *ptrB = (B *)(ptrA);
 //ptrB->call();
 //Normal compilation
 //A *ptrA = new A; 
 //B *ptrB = static_cast<B *>(ptrA);
 //ptrB->call();
 //Compile does not pass, prompt:
 //In function  ' int main()':
 //error: invalid static_cast from type  ' A*' to type  ' B*'
 
 //char c;
 //char *pC = &c;
 //int *pInt = static_cast<int *>(pC);
 //Invalid static_cast from type 'char*' to type' int*'
 //int *pInt = reinterpret_cast<int *>(pC);
 //Normal compilation . 
 //int *pInt = (int *)(pC);
 //Normal compilation . 
 
 return 0;
}

After analyzing static_cast, dynamic_cast and reinterpret_cast, the following diagram can be drawn to make a simple comparison of the differences between them. Const_cast is not included here because it is special and is covered in sections.


     ----------------
     /  dynamic_cast  --> Common inheritance system ( virtual ) [ A more secure downcast]
    ~~~~~~~~~~~~~~~~~~~~  
    /   static_cast   --> The base type [ A more secure ] , a class pointer or reference to the same inheritance scheme 
   ~~~~~~~~~~~~~~~~~~~~~~~~
   /  reinterpret_cast   --> with C-Like The action is consistent, without any static or dynamic checking mechanism 
  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  /     C-Like       --> Base type, class pointer or reference to the same inheritance scheme , Pointers or references to different inherited architecture classes 
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

4 const_cast

Const_cast can be used to remove or add the const attribute of a variable. At first, I thought this const_cast was weird. There is nothing like this in C to remove the const attribute. As a matter of fact, I have no roots in this idea itself. Later, in C++, it was always advocated to declare constants as const, so that once the constants became too many, it was inevitable to encounter the problem of the cast const property when connecting with other software components or third-party libraries. Such as:


const int myConst = 15;
int *nonConst = const_cast<int *>(&myConst);

void print(int *p)
{
  std::cout << *p;
}

print(&myConst); //Invalid conversion from 'const int*' to' int*'
print(nonConst); //normal

However, when using const_cast you should be careful not to change its value if you don't have to:


const int myConst = 15;
int *nonConst = const_cast<int *>(&myConst);

*nonConst = 10;
//If the variable is stored in readonly memory, errors may occur at run time.

5 subtotal

In C++, c-like conversions are sufficient for most data types. However, many people have been advocating for explicit data type conversions using C++ specified type conversions whenever possible. I think there are two reasons:

First, C++ is a "new" programming language that should learn to use its own ideas to solve programming problems.
The second is that, while the c-like transformation operation is powerful, if used arbitrarily, it can be hidden at compile time but invisible at run time. These problems make the behavior of the software very unclear.
In this way, C++ introduces four other types of conversions to perform certain types of conversions more safely. For example, using reinterpret_cast indicates that you definitely want to use a c-like cast; When using static_cast, you want to ensure that the converted objects are basically compatible. For example, you cannot convert a char * to an int *, and you cannot convert between Pointers or references of different inheritance system classes. Dynamic_cast is used to perform downcast transformations on classes under virtual inheritance, and it is clear that current performance is not a major factor...

Answer the question above. It can be said that for all the transformations that const_cast, static_cast, reinterpret_cast, and dynamic_cast can perform, c-like can also perform. However, c-like transformations do not have the compile-time type detection and run-time type detection provided by static_cast, and dynamic_cast.

Dr. Bjarne Stroustrup, the father of C++, also makes his point here, with two main points: first, c-like casts are extremely destructive and rarely searched for in code text; Second, the new cast makes it easier for programmers to use them and for compilers to find more errors. Third, the new cast conforms to the template declaration specification, allowing programmers to write their own casts.


Related articles: