The rules of C++ are designed to guarantee that type errors are impossible. Unfortunately, casts subvert the type system. That can lead to all kinds of trouble. Casting is a fundamental concept but it should be used with caution. A good software design helps to reduce the number of needed casts but sometimes they are needed. Within this article I want to analyze the different types of casting, how they are used and what kinds of issues may occur on unconsidered use of casts.
Types of casts
In c++ applications, you can find some different kinds of casts. There are implicit casts, c style casts, c++ functional cast expressions and c++ style casts. The following source code shows a conversion from a float into an int value by using the different kinds of casts.
int _tmain(int argc, _TCHAR* argv[]) { float a = 17.4f; int b; b = a; // implicit conversion b = (int)a; // c style cast b = int(a); // c++ functional cast expression b = static_cast<int>(a); // c++ style cast return 0; }
At the end of the article we will see this example again and identify the cast type to prefer. Now, let’s start by thinking about the different types of casts and understand their behavior.
The implicit cast “b = a” may or may not have side effects depending on the types of a and b. For example, an implicit cast from float to int results in a loss of data precision. As this may not be intended by the developer most compilers will generate compiler warnings in this case.
C style casts “(T)expression” are available to ensure downward compatibility with c. Unlike in c++, there was only one operator in c to convert data types. Since c does not know any classes and therefore no methods, the type conversion is correspondingly limited to the base data types. In c++, the c cast was extended to the c++ structures. Thus, you can do almost anything with a c cast as with one of the four c++ cast operators.
The functional cast expression used in this example “T(expression)” consists of a simple type specifier followed by a single expression in parentheses. This cast expression is exactly equivalent to the corresponding c style cast. Furthermore, the functional cast expression is available in some other variants, for example more than one expression can be used inside the parentheses. This cast looks like a constructor. And often it is as it results in a constructor call. Conversion is a form of initialization. When a type is implicitly convertible to another, a functional cast is a form of direct initialization. The compiler knows which types are convertible.
The c++ style cast – also called new style cast – offers four new cast forms: const_cast(expression), dynamic_cast(expression), reinterpret_cast(expression) and static_cast(expression). The c++ style cast will lead to compiler errors when used incorrectly. In addition, readers of the source code are roughly informed about the intention of the cast as one out of the four cast forms is explicitly used.
In c++ you should use the new style casts. We will see explanations for this guideline within the further course of the article. Therefore, the following paragraphs are focused on the c++ style casts. At first, we want to understand the for new cast forms. Afterwards, we will come back to the c style cast and see some of the issues which may occur and how these are avoided by using the new style casts.
static_cast
The static cast operator is the most popular and most commonly used one. It converts data type by using an existing conversion rule. Such a rule may be for example a constructor or an overloaded cast operator. The static cast is done at compile time.
The elementary data types can be largely converted into one another. However, this also clearly shows the greatest disadvantage of a type transformation: it almost always goes with data loss. For example, if you convert a floating-point number to an integer, all decimal places are lost.
The following example shows a typical use case for the static cast operator: a division of two integers where the result shall be a floating-point number.
int _tmain(int argc, _TCHAR* argv[]) { int x = 10; int y = 11; double z = x / y; std::cout << z << std::endl; z = static_cast<double>(x) / y; std::cout << z << std::endl; return 0; }
The first output is “0” because the implicit cast will remove the decimal places. The second output is “0.9” because with the static cast the variable was casted to a floating-point number.
The static cast can be used to force implicit conversions like from int to double or from non-const to const objects. So, you can use it to add constness to an object. Furthermore, the static cast can be used to perform conversions like void* pointers to typed pointers and pointer-to-base to pointer-to-derived. The following example shows such a type conversion from a derived child class to the base class.
class Animal { public: virtual void Print() const { std::cout << "animal" << std::endl; }; }; class Bird : public Animal { public: void Print() const { std::cout << "bird" << std::endl; } }; int _tmain(int argc, _TCHAR* argv[]) { Bird bird = Bird(); Animal animal = static_cast<Animal>(bird); bird.Print(); animal.Print(); return 0; }
Within the above example we converted an object of child class to an object of a base class. What do you think will happen if we use pointers instead and additional add to opposite use case and convert from base to child? The following example will show some interesting results.
class Animal { public: virtual void Print() const { std::cout << "animal" << std::endl; }; }; class Bird : public Animal { public: void Print() const { std::cout << "bird" << std::endl; } void Print2() const { std::cout << "bird2" << std::endl; } }; class Car { public: void Print() const { std::cout << "car" << std::endl; } }; int _tmain(int argc, _TCHAR* argv[]) { Bird* pBird = new Bird(); Animal* pAnimal = static_cast<Animal*>(pBird); // this works pBird->Print(); pAnimal->Print(); //pAnimal->Print2(); // ERROR - Won't compile pBird = static_cast<Bird*>(pAnimal); // this works pBird->Print(); pAnimal = new Animal(); pBird = static_cast<Bird*>(pAnimal); // this works pBird->Print(); pBird->Print2(); //Car* pCar = static_cast<Car*>(pBird); // ERROR - Won't compile return 0; }
The application creates the following output:
bird
bird
bird
animal
bird2
We can see that the first conversion converts the pointer to a base class pointer. Of course, we cannot call the child class method “Print2” but if we call “Print” we can see that the child class function is still used. At next we convert this pointer back to the child class pointer which is fine as the underlying object is of type “Bird”. Within the last use case we create a base class and convert the pointer of the base class to a pointer of the child class. This will work but interestingly the function calls will result in a call of the base class function “Print” and the child class function “Print2”. This example will show a very important fact: you can use static cast operator to cast object pointers and to cast a base class pointer to a child class pointer, but this may result in unexpected or even undefined behavior. Later, if we look at the dynamic cast, we will see another example which compares dynamic and static casts in case of converting from base to child class. You may now think the compiler should show a warning or error in this case, but the compiler will allow downcasts to derived class. It cannot give compile time error because a base-derived relationship can exist at runtime depending on the address of the pointers being casted. Therefore, such static casts always succeed at compile time, but may raise undefined behavior at runtime if you don’t cast to the right type. If we want to detect and avoid casting errors at runtime, we must use a dynamic cast.
dynamic_cast
The dynamic cast operator is the only cast operator which is performed at runtime. This will allow a to write code which depends on the runtime state of a variable. Pointers and references can contain objects of their own type and objects of a derived class type. Therefore, it is possible, that the type of a variable is not known at compile time because at runtime the variable may be of the given type or of one of the existing derived types. The dynamic cast operator allows a safe downcast of a variable in one of the derived types.
The dynamic cast is the only cast operator which cannot be performed using the old c style syntax. It is also the only cast that may have a significant runtime cost. Therefore, you should not use it in situations where a static cast is sufficient.
If you want to downcast types of an inheritance architecture by using the dynamic cast, you must implement polymorph classes, which means they must have at least one virtual method. For example, you can write a virtual destructor.
The following source code shows an object hierarchy with polymorph classes.
class Animal { public: virtual void Print() const { std::cout << "animal" << std::endl; }; }; class Bird : public Animal { public: void Print() const { std::cout << "bird" << std::endl; } }; class Fish : public Animal { public: void Print() const { std::cout << "fish" << std::endl; } };
We can use these classes to write an example application. Within this example we will write a function with an input parameter of type “Animal”. Of course, we can pass variables of the derived classes “Bird” and “Fish” to this function too. Within the method we can convert the animal-object back to the derived class by executing a downcast using the dynamic cast operator. If the cast is successful, the result is a pointer to the object but now it is of the type given within the cast. If the object has another type than given in the cast operator, the result is a null pointer. Therefore, after executing a dynamic cast, you must check whether the resulting pointer is valid or a null reference. The following source code shows an according example.
void Foo(const Animal* animal) { animal->Print(); const Bird* bird = dynamic_cast<const Bird*>(animal); if (bird) { bird->Print(); } const Fish* fish = dynamic_cast<const Fish*>(animal); if (fish) { fish->Print(); } } int _tmain(int argc, _TCHAR* argv[]) { Bird bird = Bird(); Fish fish = Fish(); Foo(&bird); Foo(&fish); return 0; }
Of course, it is also possible to use references instead of pointers. In this case the dynamic cast will throw an exception if the cast is not possible. The source code shows the adapted example.
void Foo(const Animal& animal) { animal.Print(); const Bird bird = dynamic_cast<const Bird&>(animal); //throws an exception if called with 'fish' const Fish fish = dynamic_cast<const Fish&>(animal); //throws an exception if called with 'bird' } int _tmain(int argc, _TCHAR* argv[]) { Bird bird = Bird(); Fish fish = Fish(); Foo(bird); Foo(fish); return 0; }
As mentioned above, dynamic cast are only available for polymorphic classes and dynamic cast can have significant runtime costs. This is because the RTTI (run-time type information) features are needed to safely convert the types. In practice, you will often have polymorphic classes anyway because base classes must have a virtual destructor to allow objects of derived classes to perform proper cleanup if they are deleted from a base pointer. So, this is not a limitation. But due to the runtime costs you should not execute dynamic casts if they are not needed.
The above source code shows a typical situation where dynamic cast is used. You will find such method in a lot of example applications, tutorials, books and of course in professional applications. But I think the method shown in the example is an example for bad software design too. The interface of the function tells the client that it expects an “Animal” object, but the implementation expects a “Bird” or “Fish”. This contradiction between interface design and functionality can lead to many issues and even unexpected behavior of your application. As this article is about casting and not about software design I don’t want to go further into this topic. But you should keep in mind, and that’s true for all four cast operators, that the need of a cast operator may be an indication for a bad software design. In source code with a clean design there is nearly no need for the cast operators.
Within the previous chapter, about the static cast, I mentioned that the static cast should not be used for a downcast. Therefore, to finish this topic we can test the behavior of the example application in case we use the static cast instead of the dynamic cast. The following source code shows a possible modification of the example application.
void Foo(const Animal* animal) { const Bird* bird = static_cast<const Bird*>(animal); if (bird) { bird->Print(); } const Fish* fish = static_cast<const Fish*>(animal); if (fish) { fish->Print(); } } int _tmain(int argc, _TCHAR* argv[]) { Bird bird = Bird(); Fish fish = Fish(); Foo(&bird); Foo(&fish); return 0; }
This looks like a valid implementation and it will compile without errors. It even cannot give compile time errors because a base-derived relationship can exist at runtime depending on the address of the pointers being casted. Static cast always succeeds, but will raise undefined behavior if you don’t cast to the right type. If you execute the example application you will get outputs you may not expect.
Preview on part 2
As we have seen two of the four cast operators now, I want to finish this first part of the topic. Within a next blog article, I want to continue this topic and explain the const cast operator and the reinterpret cast operator. Furthermore, I want to include a comparison of dynamic cast and static cast based on an example with base-to-derived and derived-to-base class conversion. Moreover part 2 will contain some more examples showing the disadvantages of the c style cast.