Contents
Dipping my toes into a new project, I got a bunch of ugly warnings about a ton of C-casts inside a macro definition. Trying to get away from them was not as easy as I first thought.
The compiler emitted a little over 1000 warnings – or, more precisely, the same warning 1000 times. Looking at the code in question it would be something innocent like this:
someFunc(FOO);
someOtherFunc(BAR->i);
Both lines do not really look like there is a cast going on. But wait – the all-caps FOO
and BAR
look suspicious. Finding the definitions took a while – we’re using an IDE for embedded development, and it’s not blessed with working functionality like “jump to definition”.
The definitions of FOO
and BAR
then looked like this:
#define FOO ((uint8*)0xBAD50BAD)
#define BAR ((S*)FOO)
Where uint8
is a typedef to some 8-bit unsigned type, and S
is a struct. There they were, the C-style casts. And to not break the C style, the writer of that code used macros instead of constant expressions.
To be fair, a handful of those macros were in actual C headers provided by third parties, but many of them appeared to be written only in the same style in a project that specifically claims to be a C++ project.
Fixing the C-style
Most C++ developers know that #define
s are “evil” because the are simple text replacement and therefore bring problems like lacking type safety and more.
In this case, the use of macros made the problem seem worse than in actually was: Only a few dozen of those macros can result in hundreds or thousands of warnings because, after the replacement, the compiler sees that C-cast at every location the macro is used.
If we go ahead and replace the macro with a constant expression, we should get the warning at the exact location where the C-cast is written, not where the macros are expanded. While we are at it, we can replace the C-cast with the proper C++ cast, which in this case is reinterpret_cast
:
constexpr auto FOO = reinterpret_cast<uint8*>(0xBAD50BAD);
constexpr auto BAR = reinterpret_cast<S*>(FOO);
Sadly, this won’t compile, because reinterpret_cast
s are not allowed in constant expressions by the standard. Before you ask: No, we can not go back to the C-cast, because the rules say that in this case, effectively a reinterpret_cast
is performed.
What can we do?
We could stop here and give up. We could just write reinterpret_cast
in the macros and live with the fact that we have ugly macros but silenced the warnings. But that’s not too satisfying, is it?
What comes to mind is that the actual constant here is the address value, i.e. the 0xBA50BAD
, and the reinterpret_cast
s are only used in the runtime code. So we might not want to bake the cast into the constant expression.
Another point is that the constant pointers seem come in pairs relatively often: A unit8*
that seems to be used for very low level reads and writes to memory, and a pointer to the same location that interprets the data as some object like the S
above.
We probably do only want these pairs, i.e. interpreting the same address as yet something else might not be wanted. With this in mind, the question is whether we could come up with a class that
- Allows us to use
constexpr
instead of macros - Provides a
uint8*
and a pointer to some fixed other type
A class template that fulfills these requirements could look like this:
template <class T> class mem_ptr{
std::intptr_t addr;
public:
constexpr mem_ptr(std::intptr_t i) : addr{i} {}
operator T*() const { return reinterpret_cast<T*>(addr); }
T* operator->() const { return operator T*(); }
uint8* raw() const { return reinterpret_cast<uint8*>(addr); }
};
std::intptr_t
is an alias for some integer type that is large enough to hold a pointer value. Since the class holds this integer value and not a pointer value, it can be used as a constant expression. The conversions to the two pointer types still have to be done in the runtime code, so they are in functions that are not constepr
.
To use this class in the current code base, without touching any other code, we would need something like the next two lines:
constexpr auto BAR = mem_ptr<S>(0xBAD50BAD);
#define FOO BAR.raw()
Yay, no more casts in our constants. The actual pointer object is a constant expression, but we still have a macro, what about that?
Conversion to `uint*`
We could go ahead and replace our raw
function with an implicit conversion operator, but I think that is not what we should do. It would make the same constant BAR
convertible to both a S*
and a uint8*
, which can be rather confusing.
Therefore, I made the conversion to uint8*
an explicit function. I will require us to replace all the occurrences of FOO
with the call to that function, but that is positive for two reasons:
FOO
andBAR
were previously unrelated, not showing that they were addressing the same memory and the same object in different ways. Now we have one constantBAR
that we use for both ways.- Making
raw
an explicit function makes it very clear that we are accessing raw memory which may be necessary but can be an unsafe operation that should be encapsulated accordingly.
Performance
Since we are in an embedded project, memory and performance are critical. However, the indirection we have through the conversion operator and the raw
function is minimal and the function calls are inlined at low levels of optimization (e.g. -O1
on ARM GCC).
Permalink
I wrote about a different way of solving this problem:
https://kristerw.blogspot.se/2017/07/hard-coded-hardware-addresses-in-cc.html
Permalink
It’s almost as if warnings are useful sometimes 🙂