If I use C-Style casts in my C++ project, is it worth refactoring to C++ casts?
I use C-style casts in my 15K LOC C++ project, 90% of the times for casts between child and base classes.
Even when I read that it is bad to use them, and that they can result in severe errors, as they are not type safe as the C++ casts, I still feel perfectly fine and comfortable with using them.
I have not experienced a single bug in my project so far that was caused by, for example, an accidentally mistyped C-Style cast - really.
There are two main reasons I have not been using them:
- I didn't know enough about them yet
- I didn't like their syntax, they are more verbose and harder to read for me
My questions:
- (Why) Should I refactor my project to use C++-style casts?
- Why should I use C++-style casts for my future projects?
I use as good as all other advantages C++ offers me, from OOP including virtual and abstract base classes, namespaces, the STL, and so on, just not the new type casting syntax. The argument "Why aren't you just using C then?" doesn't work for me.
The main advantage of the C++-style casts is, as you've mentioned, the type safety. Each cast in C++ handles one specific sort of conversion (or a family of related conversions) and so the compiler can go and check that you're not accidentally doing more conversions that you intended, or a sequence of conversions that fundamentally isn't safe.
One thing to think about is that while it's great that you feel comfortable using C-style casts, and while it's great that you haven't made any mistakes with them, other people working on the code base might not be as facile with these casts as you are. Using the casting operators makes the code more self-documenting. If you have a C-style cast somewhere, someone else reading the code might not be able to immediately infer what it is that you're doing. If they see something like
T* ptr = (T*) var;
They might not immediately be able to tell if this is
- A cast from a base class to a derived class or the other way around.
- A cast to strip
const
ness off ofvar
- A cast from an integral type to a pointer.
While they can probably glean this from context, it's a lot more obvious what's going on if you use a cast like
T* ptr = static_cast<T*>(var);
or
T* ptr = const_cast<T*>(var);
Another reason to prefer the C++-style casting operators is that they make the code more resilient to change. For example, suppose I have this function:
void DoSomething(Base* ptr) {
Derived* derived = (Derived *) ptr;
DoSomethingElse(derived);
}
Now, suppose that I realize that this function isn't supposed to make any changes to its argument, so I decide to mark it const
. For example:
void DoSomething(const Base* ptr) {
Derived* derived = (Derived *) ptr;
DoSomethingElse(derived);
}
But now we have a problem - my C-style cast which used to just do a downcast now also strips off const
ness. This can lead to an easy bug where DoSomethingElse
mutates the pointer I pass in, even though the DoSomething
method itself promises not to do this. If instead I wrote this code as
void DoSomething(Base* ptr) {
Derived* derived = static_cast<Derived *>(ptr);
DoSomethingElse(derived);
}
And then change the code by adding const
:
void DoSomething(const Base* ptr) {
Derived* derived = static_cast<Derived *>(ptr);
DoSomethingElse(derived);
}
Now I'll get a compiler error telling me that my old cast is broken, which can lead me to discover that there is a logical error in the code (namely, that DoSomethingElse
mutates its argument, so I can't naively make ptr
a pointer-to-const
.
So in short, using the C++ casting operators makes the code more readable and more maintainable. It makes the logic behind the code more explicit. And it makes the code less bug-prone by having the compiler catch errors either as you're making them or later on as you go back and change old code. I would recommend trying to use C++-style casting operators in the future for these main reasons.
As for whether you should go back and try to replace your current C-style casts with C++-style casts, that's really up to you. As an exercise, I'd suggest doing it just to practice learning what sorts of casts you're using. Plus, you might find a logic error in there, which would make the search worth your while!
In a company I used to work for, we once had to port a several million lines of code app to an new platform. What started out as "just another port to just another Unix platform" (the code already run on Windows, OSX, and half a dozen Unix-like platforms), turned out a huge problem. IIRC, the reason was that this platform had rather strict alignment requirements, and some of our casts were tripping hardware exceptions due to misalignment.
This would have been easy enough to fix, if it weren't for the fact that a certain percentage of these casts were C-style casts. It was impossible to effectively grep for these. Searches turned up literally tens of thousands of lines of code, which all had to be inspected manually by human beings, 99.99% of which were totally irrelevant.
It didn't help that us humans tended to miss a few of these hard-to spot casts, which would then have to be found in another round of reviewing tens of thousands of lines of code, 99.99% of them exactly the same as in the first round.
That task was nigh impossible to finish in the time given, and almost broke the company's back, because they were facing a severe contract penalty if we slipped the deadline.
Needless to say that afterwards it was decided to replace all C-style casts by their C++ counterpart. However, had this policy existed before we had to make that port...
(Why) Should I refactor my project to use C++-style casts?
Yes. For the same reason you should use them in the future.
Why should I use C++-style casts for my future projects?
The list is actually fairly long but I'll go over the most important.
First, they clearly state the intent of the cast. Reading "static_cast" you know that the author intended to perform a static cast, rather than a reinterpret or const.
Second, they do one, and only one thing. You can't accidentally cast away constness for example.
They're easy to search for. Search for "const_cast" to find all casts to/from const. How would you do this with C-style? You couldn't.
They don't change cast kind when local code semantics change. For example, (Widget*)(blah)
will silently change to a reinterpret cast if Widget stops inheriting from whatever type blah was a pointer to. It also changes to a reinterpret cast if the Widget definition is not locally available. If you use new-style casts instead you'll get compiler vomit rather than a silent reinterpretation of the statement.
You can do a dynamic cast. C-style casts can't do this.