C++ Casting, or: "Oh No, They Broke Malloc!"

Updated: 2018-11-14

The first time you try compiling your C code with a C++ compiler can be terrifying. One of the most troublesome offenders is malloc, and you will have your eyes opened to the number of implicit type conversions in your code.

malloc: C vs C++

Consider the following:

int * p = malloc(10);

Recall that malloc returns a void *. The C compiler will happily convert the void * result into whatever type you require. Lines of code like this are likely scattered throughout your C projects.

The C++ compiler is not as kind. Unlike C, the C++ compiler allows implicit conversions TO a void * type, but to convert FROM a void * type requires an explicit cast.

error: cannot initialize a variable of type 'int *' with an rvalue
      of type 'void *'
        int * p = malloc(10);
              ^   ~~~~~~~~~~
1 error generated.

So what can we do about this?

The first thing that comes to your mind is our friend, the C-style cast:

int * p = (int*)malloc(10);

This will work, but this style of cast is not recommended in C++. There are more explicit methods which allow us to describe the intention of our cast.

C++ Casts

C++ provides a variety of ways to cast between types:

  • static_cast
  • reinterpret_cast
  • const_cast
  • dynamic_cast
  • C-style casts

Each of the C++ casts has the following generic form:

cast_name<cast_to_type>(item_to_cast)

Let's look at what each of these casts do.

static_cast

static_cast is the main workhorse in our C++ casting world. static_cast handles implicit conversions between types (e.g. integral type conversion, any pointer type to void*). static_cast can also call explicit conversion functions.

int * y = static_cast<int*>(malloc(10));

We will primarily use it for converting in places where implicit conversions fail, such as malloc.

There are no runtime checks performed for static_cast conversions.

static_cast cannot cast away const or volatile.

reinterpret_cast

reinterpret_cast is a compiler directive which tells the compiler to treat the current type as a new type.

You can use reinterpret_cast to cast any pointer or integral type to any other pointer or integral type. This can lead to dangerous situations: nothing will stop you from converting an int to a std::string *.

You will use reinterpret_cast in your embedded systems. A common scenario where reinterpret_cast applies is converting between uintptr_t and an actual pointer or between:

error: static_cast from 'int *' to 'uintptr_t'
      (aka 'unsigned long') is not allowed
        uintptr_t ptr = static_cast<uintptr_t>(p);
                        ^~~~~~~~~~~~~~~~~~~~~~~~~
1 error generated.

Instead, use this:

uintptr_t ptr = reinterpret_cast<uintptr_t>(p);

reinterpret_cast cannot cast away const or volatile.

const_cast

const_cast adds or removes const from a variable. Strangely enough, you can also use const_cast to add or remove volatile from a variable. No other C++ cast can add or remove these keywords.

Use this cast carefully. If you declared the original variable as const, it's not safe to remove const and start modifying the underlying data.

You should primarily use const_cast to add const to a variable (such as for a function overload to use a const version). If you need to remove const from a variable, I recommend stopping and thinking about why you are in this situation.

const_cast and volatile

Removing volatile from a keyword is definitely more common in embedded systems than removing const. If you decide to use the volatile variable as a function parameter, you will need to remove the keyword.

Consider the following contrived example:

void test(int * x)
{
  //do something
}

int main(void)
{
  volatile int p = 0;
  test(&p);

  return 0;
}

In C, this example would compile. C++ throws an error:

test.c:14:2: error: no matching function for call to 'test'
        test(&p);
        ^~~~
test.c:5:6: note: candidate function not viable: 1st argument ('volatile int *')
      would lose volatile qualifier
void test(int * x)
     ^
1 error generated.

To prevent this error, we will need to use const_cast:

test(const_cast<int*>(&p));

dynamic_cast

I have never used dynamic_cast in my embedded projects, and I usually keep run-time-type-information (RTTI) disabled. I will provide a cursory overview, but please see "Further Reading" below for more detailed information.

We use dynamic_cast to handle polymorphism. dynamic_cast can convert pointers and references to any polymorphic type at run-time, primarily to cast down a type's inheritance hierarchy. If dynamic_cast can't find the desired type in the inheritance hierarchy, it will return nullptr for pointers or throw a std::bad_cast exception for references.

C-style Casts

A C-style cast in C++ tries the following casts in order, using the first C++ cast that works:

  1. const_cast
  2. static_cast
  3. static_cast, then const_cast (change type + remove const)
  4. reinterpret_cast
  5. reinterpret_cast, then const_cast (change type + remove const)

Note that dynamic_cast is never considered when using a C-style cast.

Casting Recommendations

Five casts is a lot to keep in mind. Here are some quick rules of thumb for these new casts:

  1. Use static_cast for your ordinary conversions
  2. Use reinterpret_cast for specific cases were you need to reinterpret underlying data (e.g. converting from a pointer to uintptr_t)
  3. Use dynamic_cast for converting pointers and references along an inheritance hierarchy
    1. Only use dynamic_cast on classes with virtual members
  4. Use const_cast when you need to remove const or volatile keywords. Think carefully before using this cast.
  5. Avoid C-style casts. Be explicit in your intentions.

One unappreciated benefit of using C++-style casts is you can search through your code and immediately locate all casts. With C-style casts, you cannot easily tell the difference between a declaration and a cast if you are performing a search.

Updating Our malloc Calls

That was quite the long detour!

Coming back to our malloc example, we can now see that we should use a static_cast:

int * p = static_cast<int*>(malloc(10));

All that casting really is a nuisance, isn't it?

Further Reading

If you find C++ casting still to be unclear, try the following two links:

These are useful references if you need to refer to API documentation:

Change Log

  • 20181114:
    • Removed misleading note about new being a type-safe malloc alternative
    • Improved grammar
    • Fixed typos
    • Links now open in new tab