Casting in C++

When you write code in C++, you will probably get to a point in which you need to convert(or cast) some data type.

Maybe you need to convert a double or a float to integer or maybe you need to convert one pointer type to other pointer type.

Enters casting.

What is casting?

Casting is a conversion process wherein data can be changed from one type to another.

I’ve also used casting in previous articles – for e.g. in the “Performance of dynamic polymorphism” article, when I gave the static polymorphism example.

C++ has two types of conversions: implicit and explicit.

Implicit conversion is the one performed by the compiler directly, without us specifying it.

Explicit conversion is conversion performed by the compiler only when we specify that it should.

Casting is a form of explicit conversion. I, as a programmer, tell the compiler to convert some data types.

Types of casting

In C++ there are 5 types of casting. Actually, there are only 4 in the real sense since the 5th one doesn’t actually have it’s own functionality but uses the other 4. However, we will also discuss about that one in this article.

The 5 types of casting are:

  • const_cast
  • dynamic_cast
  • static_cast
  • reinterpert_cast
  • explicit cast (or C-style cast)

const_cast

It converts between types with different cv-qualification and it is the only one that can cast away(remove) constness or volatility.

// const pointer to integer
int const* cptr = new int(1);
// constness casted away
// simple pointer to integer
int* ncptr = const_cast<int*>(cptr);

// volatile pointer to integer
int volatile* voptr = new int(1);
// volatility casted away
// simple pointer to integer
int* nvoptr = const_cast<int*>(voptr);

Above example illustrates this with pointer types, but it works the same way for references.

// variable to reference to
int x = 1;

// const reference to integer
int const& cref = x;
// constness casted away
// simple reference to integer
int& ncref = const_cast<int&>(cref);

// volatile reference to integer
int volatile& voref = x;
// volatility casted away
// simple reference to integer
int* nvoref = const_cast<int&>(voref);

It cannot be used on:

  • pointers to functions
  • pointers to member functions.

Modifying a const object through a non const access path or referring to a volatile object through a non-volatile glvalue results in undefined behavior.

dynamic_cast

Safely converts pointers and references to classes up, down, and sideways along the inheritance hierarchy.

// pointer to Base object
Base* bptr = new Base;

// dynamic cast
Derived2* d2ptr = dynamic_cast<Derived2*>(bptr);
Derived1* d1ptr = dynamic_cast<Derived1*>(d2ptr);

// free memory
delete bptr;

// Base object
Base b;

// reference to base object
Base& bref = b;

// dynamic cast
Derived2& d2ref = dynamic_cast<Derived2&>(bref);
Derived1& d1ref = dynamic_cast<Derived1&>(d2ref)

If the cast fails and the new type is

  • a pointer type, it returns a null pointer of that type
  • a reference type, it throws an exception of type std::bad_cast

If expression is a pointer to a polymorphic type, the new type is void*, the result is a pointer to the most derived object pointed or referenced by expression.

When used in a constructor or destructor and if the new type is not a pointer or reference to the constructor’s or destructor’s own class or one of it’s bases, the behavior is undefined.

static_cast

Converts between types using a combination of implicit and user-defined conversions.

May involve implicit conversions, a call to the constructor of a new type or a call to a user-defined conversion operator.

double f = 0.1;
// implicit conversion
int x = static_cast<int>(f);


class Int {
public:
    Int(int a = 0)
        : a_{a}
    {
        std::cout << "Conversion Constructor" << std::endl;
    }
    operator std::string()
    {
        std::cout << "Conversion Operator" << std::endl;
        return std::to_string(a_);
    }
private:
    int a_;
};

Int obj(1);
// conversion operator
std::string str2 = static_cast<std::string>(obj);
// conversion constructor
obj = static_cast<Int>(2);

In case of downcast conversion, it makes no runtime checks to ensure that the object’s runtime type is actually the one requested.

// pointer to Base object
Base* bptr= new Base;

// no runtime checks done
Derived* dptr = static_cast<Derived*>(bptr);

// free memory
delete bptr;

May also be used to disambiguate function overloads by performing a function-to-pointer conversion to specific type.

std::for_each(files.begin(), files.end(),
    static_cast<std::ostream&(*)(std::ostream&)>(std::flush));

reinterpret_cast

Converts between types by reinterpreting the underlying bit pattern.

Does not compile to any CPU instructions.

// pointer to object of type B
B* b = new B;

// converting B to A
A* a = reinterpret_cast<A*>(b);

// free memory
delete b;

Cannot be used to interpret the bytes of an object as a value of a different type – std::memcpy or std::bit_cast (C++20) should be used in such case

double d = 0.1;
std::int64_t n;
static_assert(sizeof(n) == sizeof(d));

// Undefined behavior
// n = *reinterpret_cast<std::int64_t*>(&d)

// proper usage
std::memcpy(&n, &d, sizeof(d));
n = std::bit_cast<std::int64_t>(d)

Explicit cast (C-Style)

double f = 0.1;
// C-Style cast
int x = (int)f;

The compiler attempts to interpret the C-Style cast as the following cast expressions, in order:

  • const_cast
  • static_cast with extensions
  • static_cast followed by const_cast
  • reinterpret_cast
  • reinterpret_cast followed by const_cast

Usage of C-Style cast is not encouraged since it is very hard to find in the code if problems arise and it’s “too powerful”.

Leave a Reply

%d bloggers like this: