Global training solutions for engineers creating the world's electronics

Introduction to C++ Casting Issues

The purpose of this article is to clear up issues with casting in C++.

There are many times when calling subroutines or assigning results from returns, the situation arises that types don't match, and C++ strict typing appears to get in the way. There are several solutions to this problem, including implicit conversions, explicit conversions, and casting. Some of these are more dangerous than others. This article attempts to clarify and guide you to make good decisions.

Some general comments are appropriate before proceeding.

First, good C++ programmers avoid casting whenever possible. Casting has terrible results, including runtime overhead and incorrect conversions, unless adequately understood.

Second, these same programmers rightfully take a dim view of any casting they see during code reviews unless that casting is properly justified with comment blocks.

Third, be afraid whenever you see or feel the need for casting.

Conversions

Conversions come in two varieties. Implicit conversions include things like int to float and visa versa.

4
1
    double d1 = 3.14159, d2;
2
    int i1 = 42, i2;
3
    d2 = i1; // implicit conversion
4
    i2 = f1; // implicit conversion

Unfortunately, implicit conversions can get you in trouble. For instance, char is considered to be an 8-bit integer. Consider the following legal, but highly likely bug:

1
1
    int buffer['t']{}; // an array of 116 integers - silently compiles -- bug?

To avoid this problem, you can use explicit conversions using the target type as an operator function. For instance:

4
1
int i;
2
float pi(3.14159);
3
i = int(pi); // explicit conversion
4
int buffer[int('t')]{}; // only slightly better than previous example

When creating classes, you should create needed conversions. Single argument constructors act as conversions to a class, which must also be designated as explicit. You can use operator definitions to define conversions to other classes. With modern C++, you should also require single argument operators to be explicit by adding the explicit keyword. In the following, omit the keyword explicit for operators for C++ before C++11.

44
1
static_assert( __cplusplus >= 2014L, "Requires C++14" );
2
using std::literals;
3
 
4
class Polar; //< forward declare
5
class Rect {
6
public:
7
  using std::string;
8
  Rect(double x, double y) : m_x{x}, m_y{y} {}
9
  Rect() = default; // default constructor
10
  explicit Rect(double x) : Rect{x,0.0} {} // explicit constructor
11
  explicit Rect(const Polar& p); // explicit conversion
12
  explicit operator double() const { return m_x; } // explicit conversion
13
  [[nodiscard]] std::string string() const {
14
    return "R{ "s + std::to_string(m_x) + ", "s + std::to_string(m_y) + " }"s;
15
  }
16
  friend std::ostream& operator<<(std::ostream& os, const Rect& rhs) {
17
    return os << rhs.string();
18
  }
19
  [[nodiscard]] double magnitude() const { return std::sqrt(m_x*m_x+m_y*m_y); }
20
  [[nodiscard]] double angle() const { return std::atan2(m_y,m_x); }
21
private:
22
  double m_x{}, m_y{};
23
};
24
 
25
class Polar {
26
public:
27
  Polar(double r, double a): m_r{r}, m_a{a} {}
28
  Polar() = default; // default constructor
29
  explicit Polar(double r) : Polar{r,0.0} {}
30
  explicit Polar(const Rect& r) : Polar{r.magnitude(), r.angle()} {}
31
  explicit operator double() const { return m_r; } // explicit conversion
32
  [[nodiscard]] double run() const { return m_r * std::sin(m_a); }
33
  [[nodiscard]] double rise() const { return m_r * std::cos(m_a); }
34
  [[nodiscard]] std::string string() const {
35
    return "P{ "s + std::to_string(m_r) + ", "s + std::to_string(m_a) + " }"s;
36
  }
37
  friend std::ostream& operator<<(std::ostream& os, Polar const& rhs) {
38
    return os << rhs.string();
39
  }
40
private:
41
  double m_r{}, m_a{};
42
};
43
 
44
Rect::Rect(const Polar& p) : Rect{ p.run(), p.rise() } {}

Sometimes, a conversion does not exist, but you believe you know more than the compiler. In these situations, it may be appropriate to use a cast. For these situations, C++ supplies four types of casting with various levels of safety.

C-style casts - part 1

C provides a cast of the form (TYPE)VALUE, but this is extremely dangerous! There are two reasons. First, it says, I know more than the compiler. Second, because the syntax is terse (few characters), it is easy to overlook. Consider the following disaster:

3
1
  char  *pc = "HelloWorld";
2
  float *pf;
3
  pf = (float*)pc; //< DANGEROUS cast - do you know the internal format of floats?

Smart C++ programmers NEVER use C-style casts. There is another section on this ahead. Read on…

Static cast

static_cast<T> is the first cast you should attempt to use. This type of cast is called static because the C++ standard requires compilers to validate static_casts at compile-time. If the compiler cannot resolve the type conversion as valid, then it won't compile. The T is a type name such as int, a class name, or a struct name.

static_cast<T> does things like implicit conversions between compatible types (such as int to float or pointer to void*), and it can also call explicit conversion functions (or implicit ones).

In many cases, explicitly stating static_cast<T> isn't necessary. Still, it's important to note that the T(something) syntax may be equivalent to (T)something (see ISO-IEC-14882-2011 section 5.2.3) and should be avoided (more on that later). A T(something, something_else) (i.e., two or more arguments) is safe and guaranteed to call a constructor. With C++11, you may also safely use uniform initialization of the form T{something}.

static_cast<T> can also cast through inheritance hierarchies. It is unnecessary when casting upwards (towards a base class), but when casting downwards, it can be used as long as it doesn't cast through virtual inheritance, which requires dynamic_cast. However, it does not do run-time checking, and it is undefined behavior to static_cast<T> down a hierarchy to a type that isn't actually the object type. Thus static_cast<> should not generally be used for downcasting.

Const cast

const_cast<T> can be used to remove or add const to a variable, and no other C++ cast has this ability (not even reinterpret_cast). It is important to note that using it is only undefined if the original variable is const; if you use it to take the const off a reference to something that wasn't declared with const, it is safe. For instance, this can be useful when overloading member functions based on const. It can also add const to an object, such as to call a member function overload. Although occasionally good reasons exist to use const_cast<T>, many experts suggest it is dangerous. The danger comes because you can remove the const property from something that should never be changed. This will create undefined behavior.

It would be best not to use the const_cast operator to directly override a constant variable's constant status.

const_cast also works similarly on volatile, though that's less common.

Dynamic cast

dynamic_cast<T> is almost exclusively used for handling polymorphism. You can cast a pointer or a reference to any polymorphic type to any other class type (a polymorphic type has at least one virtual function, declared or inherited). You don't have to use it to cast downwards; you can cast sideways or even up another chain. The dynamic_cast will seek out the desired object and return it if possible. If it can't, it will return nullptr in the case of a pointer or throw std::bad_cast in the case of a reference.

If type-id is not void*, a run-time check is made to see if the object pointed to by the expression can be converted to the type pointed to by T.

dynamic_cast has some limitations, though. It doesn't work if you don’t use virtual inheritance and multiple objects of the same type are in the inheritance hierarchy (i.e., the so-called dreaded diamond inheritance). dynamic_castalso can only go through public inheritance - it will always fail to travel through protected or private inheritance. However, this is rarely an issue as such forms of inheritance are rare.

Although dynamic_cast conversions are safer than static_cast, dynamic_cast only works on pointers or references, and the run-time type check is overhead.

Reinterpret cast

reinterpret_cast<T> is the most severe cast and should be used sparingly. It turns one type directly into another - such as casting the value from one pointer to another, storing a pointer in an int, or all sorts of other nasty things. Essentially, the only guarantee you get with reinterpret_cast is that you will get the same value if you cast the result back to the original type. There are several conversions that reinterpret_cast cannot do, too. It's used primarily for particularly weird conversions and bit manipulations, like turning a raw data stream into actual data or storing it in an aligned pointer's low bits.

For example:

3
1
int32_t herp = 1337;
2
float* derp = reinterpret_cast<float*>(&herp); // dumb idea
3
float magic = *derp;

This is essentially how the fast inverse square root works.

C-style casts - part 2

C-style casts are casts using (type)object. A C-style cast used in C++ is defined as the first of the following which succeeds:

5
1
    const_cast<T>
2
    static_cast<T>
3
    static_cast, then const_cast<T>
4
    reinterpret_cast<T>
5
    reinterpret_cast<T>, then const_cast<T>

Because of the preceding table, C++ coders should never use C-style casting.

C-style casts also ignore access control when performing a static_cast, which means they can perform an operation that no other cast can. This is bad; therefore, avoid C-style casts.

Guidelines

  • Whenever possible, avoid using any casting. Conversion is preferable.

  • Use C++ uniform initialization syntax whenever possible to avoid implicit conversions.

  • During code reviews, be especially suspicious of casts and require documentation demonstrating the need.

  • If casting is unavoidable, then justify the decision with a well-written comment block next to every cast or group of casts.

  • Use dynamic_cast for converting pointers/references within an inheritance hierarchy. Runtime overhead is insignificant compared to the bugs it may avert.

  • Use static_cast for ordinary type conversions.

  • Use reinterpret_cast for a low-level reinterpretation of bit patterns. Use with extreme caution. This type of casting is often non-portable due to endianess issues.

  • Use const_cast for casting away const/volatile. Avoid this unless you are stuck using a const-incorrect API.

  • Use conversion operators (e.g., operator int()) or constructors (e.g., T2(T1)) when possible, but be sure they are conversions and not devolved C-style casts.

  • Don’t ever use C-style casts!

Download an expanded example

You can download an expanded version of this code here » 

In exchange, we will ask you to enter some personal details. To read about how we use your details, click here. On the registration form, you will be asked whether you want us to send you further information concerning other Doulos products and services in the subject area concerned.

References

This article was written by David C Black, Senior Member Technical Staff at Doulos. Version 1.5

This article is Copyright (C) 2018-2024 by Doulos. All rights are reserved.