(split thread) c-style casts

Started by
18 comments, last by Bearhugger 8 years, 1 month ago

I honestly can't understand why anybody recommends them over C-style casting.

Then you should consider that they were invented because people got tired of shooting themselves in the foot by “over-casting”.

L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

Advertisement
Does anyone know of an example where C-style cast causes a problem with non-pointer/non-reference types? All of the intentional problems I've tried to cause result in compile-time errors.

I honestly can't understand why anybody recommends them over C-style casting.

Then you should consider that they were invented because people got tired of shooting themselves in the foot by “over-casting”.

L. Spiro

static_cast<T> == (T)
reinterpret_cast<T*> == (T*)
const_cast<const T*> == (T*)

I've not once run into a bug caused by C style casting that would not also have been caused by one of those C++ casts. So instead of just shooting yourself in the foot, you should get three guns to choose from?


It was a sobering realisation that even floating point limits can be easily exceeded by multiplying seemingly small quantities, especially since they are used in the oft-mentioned "critical loops", "engine cores," "numeric libraries." I started appreciating things like media compression artifacts, 3D graphics clipping, statistical / mechanical errors...

Most of the time in games you're working with values -1.0 <= x <= 1, and multiplying values in that range together. This prevents the issue you noticed from happening because the result will always be in that same range. The higher precision of double might make some difference in some corner case, but generally it's not necessary. I personally have had good results in software rasterizing with a fixed point type with 15 bits of precision (8 bits less than IEEE single precision float).


I just had this naive impression that ieee float dealt with the int-overflowing-multiply-by-10 by incrementing an exponent such that it could handle much larger values; e.g. 10 to the 10 "felt" possible with little error. On the fraction part, I "knew" shifting the decimal point would favour double but a clear "error" in the fraction part when doing float(double) was a shock.

floating point exponents are applied to a base of 2, not 10, unless you specify them as decimal.
IIRC 2^127x(1-FLT_EPSILON) is the largest value possible in a float, but you'd need 128 bits to represent all numbers in this range accurately.


static_cast<T*> == (T)
reinterpret_cast<T*> == (T*)
const_cast<T*> == (const T*)

I've not once run into a bug caused by C style casting that would not also have been caused by one of those C++ casts. So instead of just shooting yourself in the foot, you should get three guns to choose from?

Just a minor complaint about your usage of const cast - const_cast<const T*> is wrong, the way you use it is actually const_cast<T*>, and thus the equivalent c-style cast is (T*) - thats one of the reasons why C-style casts can be considered bad, because you might just cast a const modifier away where you didn't expect to.

I've heard of a studio in the past few years ban C++ casts altogether due to bugs/weirdness in generated assembly.

As for "getting the wrong type of cast" with a C-style cast, you can look at the code and easily find the different between reinterpret and static cast by noticing if the cast is casting a pointer type or not. As for const cast, well that only holds merit if your code base believes in using const in the first place. I think the argument that made the most sense to me is C++ casts are way easier to parse compared to C-style casts, which makes code pre-processing potentially easier.


static_cast<T*> == (T)
reinterpret_cast<T*> == (T*)
const_cast<T*> == (const T*)

I've not once run into a bug caused by C style casting that would not also have been caused by one of those C++ casts. So instead of just shooting yourself in the foot, you should get three guns to choose from?

Just a minor complaint about your usage of const cast - const_cast<const T*> is wrong, the way you use it is actually const_cast<T*>, and thus the equivalent c-style cast is (T*) - thats one of the reasons why C-style casts can be considered bad, because you might just cast a const modifier away where you didn't expect to.

You're right, of course, I was typing that message in a hurry because I was on my way out the door.


I've heard of a studio in the past few years ban C++ casts altogether due to bugs/weirdness in generated assembly.

I can believe weirdness, but not bugs, unless it was some special-purpose compiler they were using (IE one for some specific console).


As for const cast, well that only holds merit if your code base believes in using const in the first place.

I think const serves a valid purpose in describing the usage of function arguments. Const allows optimization in some handful of cases, too. I'm not sure it's valid for much else. And really the using of const_cast generally means that the function argument you're casting should not have been declared const in the first place, it means you are breaking your promise of non-modification.


I think the argument that made the most sense to me is C++ casts are way easier to parse compared to C-style casts, which makes code pre-processing potentially easier.

So we should never use C-style casts... to shave a few microseconds off compile time?

Splitting this side-discussion off to its own thread.

edit: Really, multiquote? Yes, /quote tags are a thing...


As for "getting the wrong type of cast" with a C-style cast, you can look at the code and easily find the different between reinterpret and static cast by noticing if the cast is casting a pointer type or not.



That's not quite true. Some pointer types can be (and are) used with static_cast. You can use static_cast to convert a pointer to a base class to a pointer to a derived class, but this isn't considered especially safe. It's more accurate to think of static_cast as a way of telling the compiler to cast to the specified type in such a way that the compiler listens to the type system, while reinterpret_cast just ignores the type system and does what you ask (but only on pointer types).


I can believe weirdness, but not bugs, unless it was some special-purpose compiler they were using (IE one for some specific console).



Yep. A lot of studios avoided templates (and by extension huge swathes of the standard library) for similar reasons.


I think const serves a valid purpose in describing the usage of function arguments. Const allows optimization in some handful of cases, too. I'm not sure it's valid for much else.


It's useful for minimizing side-effects. It's harder to accidentally change something if the compiler won't let you. Some have argued that variables should be const by default, and mutability should be explicitly declared. Passing by const-reference ensures that you don't add side effects by accident where you don't need them in addition to allowing optimization.


So we should never use C-style casts... to shave a few microseconds off compile time?





Parsing is not only done by compilers. ;)

1. Suppose I wanted to check for all the places where I cast a void* to a Foo* - as opposed to casting to a Foo* from its base class, Bar. Then I'd open up my search function and search for "reinterpret_cast<Foo*>" and get what I wanted.
2. C++ casts stand out more in the code - useful because explicitly calling out casting can make certain types of bugs more obvious - off the top of my head, float -> integer conversions and signed -> unsigned conversions are good examples of that.
3. C++ casts signify intent. If I'm using reinterpret_cast, then I'm telling other programmers (not just the compiler) that my intent is to reinterpret, whereas if I'm using static_cast, I'm telling them that my intent is to do an actual type conversion.

static_cast == (T)
reinterpret_cast == (T*)
const_cast == (const T*)

This is not an accurate depiction of the casts.
A C cast is static_cast<T *>(reinterpret_cast<T *>(const_cast<T *>(X))).
It’s overkill and never leads to fewer bugs or problems than C++ casts.


L. Spiro

I restore Nintendo 64 video-game OST’s into HD! https://www.youtube.com/channel/UCCtX_wedtZ5BoyQBXEhnVZw/playlists?view=1&sort=lad&flow=grid

What's wrong with C-style casts?
#include <iostream>
 
class AbstractEntity
{
	public:
	virtual void Print() { std::cout << "AbstractEntityA" << std::endl; }
};
 
class DerivedA : public AbstractEntity
{
	public:
	void Print() { std::cout << "DerivedA" << std::endl; }
};
 
class DerivedB
{
	public:
	void Print() { std::cout << "DerivedB" << std::endl; }
};
 
int main()
{
	AbstractEntity *entityA = (AbstractEntity*)new DerivedA();
	AbstractEntity *entityB = (AbstractEntity*)new DerivedB();
 
	DerivedA *derivedA = (DerivedA*)entityB;
	DerivedB *derivedB = (DerivedB*)entityA;
 
	derivedA->Print();
	derivedB->Print();
 
	return 0;
}
This code compiles fine

If I tried to convert that to C++ casts, it thankfully wouldn't compile.

DerivedB isn't derived from AbstractEntity, so it shouldn't be able to be cast to it.
Even if DerivedB WAS derived from AbstractEntity, it still shouldn't be cast to DerivedA, nor should DerivedA be castable to DerivedB.

Luckily the code crashes (in some cases), but in complex projects much more subtle and time-consuming bugs can result.

I still like to use C-style casts for some simple conversions, out of laziness or (in some cases) succinctness for legibility, but more and more I've been forcing myself to use C++ casts even when doing basic casts, as just another way to help the compiler know my intentions so it can better help me, and another way to help my future self (or other readers of the code) know what I was intending.

This topic is closed to new replies.

Advertisement