Introduction
C++ offers stronger type checking than C. In C, many conversions can
be implicit. For instance converting void* to T*, where T is any data
structure type, is possible.
However, this practice is unsafe. If the object pointed to by the void*
pointer is not of type T, then the effect can be disastrous. While some of the
unsafe conversions used in C are still possible in C++, in most cases
they must be explicit conversions using C++-like casting operations.
Unsafe Type Casting with Primitive Types
Consider the following code snippet:
#include "c_like_casting.h"
// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(c_like_casting, CONFIG_APP_LOG_LEVEL);
void c_like_casting() {
char i = 0;
char j = 0;
char* p = &i;
void* q = p;
int* pp = q; // unsafe, legal C, not C++
LOG_DBG("%d %d", i, j);
*pp = -1; // This overwrites memory starting at &i
LOG_DBG("%d %d", i, j);
}
The int* pp = q; statement does not compile in C++. However, unsafe code
is still possible using the statement int* pp = static_cast<int*>(q);, but in
this case, the developer’s intention is explicit.
#include "c_like_casting.hpp"
// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
// stl
#include <limits>
LOG_MODULE_REGISTER(cpp_like_casting, CONFIG_APP_LOG_LEVEL);
void cpp_like_casting() {
char i = 0;
char j = 0;
char* p = &i;
void* q = p;
int* pp = static_cast<int*>(q); // unsafe, but with explicit conversion
LOG_DBG("%d %d", i, j);
*pp = -1; // This overwrites memory starting at &i
LOG_DBG("%d %d", i, j);
}
C-like vs C++-like Type Casting for Unrelated Types
In C++, classes are often used as standard or user-defined types. It is also very common to convert between types (up- or downcasting). When programming in C++, the best choice is to use C++-like type casting.
The reason is demonstrated in this code snippet:
class Base {
protected:
int32_t _d1 = 0;
public:
virtual void m1(int32_t i) {
_d1 -= i;
LOG_DBG("Base::m1(): _d1: %d", _d1);
}
};
class Derived : public Base {
private:
int32_t _d2 = 1;
public:
void m1(int32_t i) override {
_d2 += i;
LOG_DBG("Derived::m1(): _d1: %d _d2: %d", _d1, _d2);
}
void m2(int32_t i) {
_d2 -= i;
LOG_DBG("Derived::m2(): _d1: %d _d2: %d", _d1, _d2);
}
};
class Unrelated {
private:
int16_t _d3 = 1;
public:
void m() {
_d3++;
LOG_DBG("Unrelated::m(): _d3: %d", _d3);
}
};
void c_vs_cpp_like_casting() {
// Declare one variable of each type.
Base b;
Derived d;
Unrelated u;
//###################
// USING c-type cast
// Primitive types casts
{
// Using cast for Base, Derived and Unrelated
// You can cast almost anything
// This cast is accepted and correct since Base is a base class to Derived
// and Naming is a Derived instance
Base *pb = (Base *) &d;
// it calls m1 from Derived as expected.
pb->m1(1);
// This cast is accepted because a Base instance could be a Derived instance.
// It is correct because pb is effectively pointing to a Derived instance.
Derived *pd = (Derived *) pb;
// it calls m1 from Derived as expected.
pd->m1(1);
// But if pb is now pointing to a Base instance it is accepted.
// One can then call methods of Derived on a Base instance ! -> undefined behavior.
pb = &b;
pd = (Derived *) pb;
// it calls m1 from Base and not Derived...
pd->m1(1);
// it calls m2 from Derived but pd is effectively pointing to a Base
// instance
pd->m2(1);
// It is even possible to cast to a type which is totally unrelated to the expression !
Unrelated *pu = (Unrelated *) &b;
// it calls m() from Unrelated...
pu->m();
// The address of any object can be stored in almost any type (as long as it can store the address)
uint64_t dummy = (uint64_t) &b;
LOG_DBG("Dummy: %lld", dummy);
// In C++, some c-style casts that would be accepted in C are not accepted
// Here char cannot store the address to b.
// The code below does not compile.
// char ch = (char) &b;
}
//###################
// USING static_cast
// Primitive types
// Conversion with static casts behaves similarly to c-type cast.
// It does however reject casting between unrelated types !
{
// Using cast for Base, Derived and Unrelated
// static_cast prevents conversions between unrelated types.
Base *pb = static_cast<Base *>(&d);
Derived *pd = static_cast<Derived *>(pb);
pb = &b;
pd = static_cast<Derived *>(pb);
pd->m2(1);
// this may crash at run time...
// The code below does not compile
// Unrelated* pu = static_cast<Unrelated*>(&b);
// The code below does not compile
// uint64_t dummy = static_cast<uint64_t>(&b);
}
}
If you read the code and the comments carefully, you will see that some allowed, unsafe C-like casting are not allowed in C++. In particular, you can’t cast between unrelated types in C++.
If you run this code, you will see strange results in the serial terminal. Some
of the values for the class instances are wrong, which shows that memory is
being written at the wrong places. It might seem easy to spot these mistakes,
but it’s not. This might not be true, because it’s usually really hard to spot
when memory is overwritten at the wrong places. Even worse, it may lead to
different behaviours depending on the compiler and on the compiler optimisation.
If you add the configuration parameter CONFIG_DEBUG_OPTIMIZATIONS=y to the
code snippet, you should see the following output in the serial monitor:
*** Booting Zephyr OS build v4.3.0 ***
[00:00:00.253,814] <dbg> programming: c_like_casting: 0 0
[00:00:00.259,643] <dbg> programming: c_like_casting: 255 0
[00:00:00.265,655] <dbg> programming: cpp_like_casting: 0 0
[00:00:00.271,697] <dbg> programming: cpp_like_casting: 255 0
[00:00:00.277,893] <dbg> programming: m1: Derived::m1(): _d1: 0 _d2: 2
[00:00:00.284,851] <dbg> programming: m1: Derived::m1(): _d1: 0 _d2: 3
[00:00:00.291,839] <dbg> programming: m1: Base::m1(): _d1: -1
[00:00:00.298,034] <dbg> programming: m2: Derived::m2(): _d1: -1 _d2: 23140
[00:00:00.305,419] <dbg> programming: m: Unrelated::m(): _d3: -19683
[00:00:00.312,225] <dbg> programming: c_vs_cpp_like_casting: Dummy: 536875472
[00:00:00.319,824] <dbg> programming: m2: Derived::m2(): _d1: -1 _d2: 23139
CONFIG_SIZE_OPTIMIZATIONS=y. You
should observe a different behavior with a crash! A change of behavior when
changing compilation options is very difficult to debug!
Note that there are other C++ cast primitives such as dynamic_cast or
reinterpret_cast, but these are usually disabled when programming embedded
systems because of their cost.