Thread: FAQ: Difference between C and C++ style casting

  1. #1
    Registered User Queatrix's Avatar
    Join Date
    Apr 2005
    Posts
    1,342

    Thumbs down FAQ: Difference between C and C++ style casting

    I have read in multipul places that () is bad to do and that reinterpret_cast is the right way of converting varibles. Why? The compiler has no problem with me using (). So what makes it so wrong?

  2. #2
    Cat without Hat CornedBee's Avatar
    Join Date
    Apr 2003
    Posts
    8,895
    Terminology: () is a C-style cast, *_cast is the C++-style cast.

    1) Lack of differentiation. What if the C-style your cast will do whatever is necessary to achieve the target. If you meant to truncate a double to an int, it will do that. If you meant to cast a pointer to an integral type, it will do that too. If you mean to expand a short into an int explicitly to resolve ambiguity, it will do that. And if you mean to remove a const qualifier from a pointer because of a legacy function that isn't const-correct, it will do that.
    The C++-style has different keywords for different tasks. If you do a simple, rather safe cast, such as promoting short to int or truncating a double to int, you can use static_cast. Try anything more complex with it, and the compiler will complain. This ensures you don't accidently cast to some weird type when all you meant to do is truncate. E.g. look at this code:
    Code:
    void foo(double *val)
    {
      int v = *val;
    }
    This will compile, but it will probably emit a warning. Now you apply a C-style cast to tell the compiler, yes, you know it's truncating, but as fate will have it, you accidently remove the asterisk:
    Code:
    void foo(double *val)
    {
      int v = (int)val;
    }
    Now this code will compile (perhaps with a warning on 64-bit systems, perhaps not). It will also not do what you want: it will give v the value of the pointer passed in, not the pointed-to value, truncated.
    C++-style, you would have done this:
    Code:
    void foo(double *val)
    {
      int v = static_cast<int>(val);
    }
    This won't compile: static_cast cannot cast from pointer types to integral types. You just saved yourself from a very hard-to-find bug.
    The second cast operator, const_cast, applies to our last example. It will cast the const and volatile modifiers from and to types. (Of course, the addition of them is implicit, so they're rarely used this way.)
    Code:
    #include <tchar.h>
    
    // Function guarantees not to modify the string, but alas, nobody made the parameter const.
    void legacy(char *str);
    
    void foo(const _TCHAR *txt)
    {
      legacy(txt); // Doesn't compile.
    }
    _TCHAR is a special type defined in tchar.h: it is a typedef for char normally, but if the macro _UNICODE is defined, it becomes a typedef for wchar_t. This is very useful in developing for Win9x, 2k and CE at the same time.
    OK, once again, you solve this with a C-style cast:
    Code:
    #include <tchar.h>
    
    // Function guarantees not to modify the string, but alas, nobody made the parameter const.
    void legacy(char *str);
    
    void foo(const _TCHAR *txt)
    {
      legacy((char*)txt);
    }
    Now the code compiles. And now it's freaking dangerous: you forgot that _TCHAR might be wchar_t at one point, so you fail to provide for that case, where you should actually call wlegacy(), not legacy(). What happens when someone compiles as Unicode?
    Code:
    #define _UNICODE
    #include <tchar.h>
    
    // Function guarantees not to modify the string, but alas, nobody made the parameter const.
    void legacy(char *str);
    
    void foo(const _TCHAR *txt)
    {
      legacy((char*)txt);
    }
    Answer: it still compiles. And legacy() will be very confused by getting a rather meaningless sequence of bytes: typically (if the first character of the string has a code point within 0-255, as would be the case for English text) it will see just the first character of a string, followed by a null byte.
    Had you used a const_cast, it would now fail and you would be alerted to the fact that you have to account for these circumstances:
    Code:
    #include <tchar.h>
    
    // Function guarantees not to modify the string, but alas, nobody made the parameter const.
    void legacy(char *str);
    
    void foo(const _TCHAR *txt)
    {
      legacy(const_cast<char*>(txt)); // Compiles only if _TCHAR is char.
    }
    reinterpret_cast, finally, is the thing that will do just about anything (except the safe casts the other operators are for). And having to use reinterpret_cast is a good sign that somewhere there might be a design issue.

    2) Templates. When you don't know the actual types you get, limiting the stuff a cast may do is more important than ever.

    3) Searchability. Imagine your program crashes with an access violation: a pointer is set to nirvana. Now there's typically four causes for this kind of bug.
    a. Uninitialized variables.
    b. Referencing pointers after they've been deleted.
    c. Overwriting the pointer due to a buffer overflow.
    d. Bogus casts from integral types.
    The first three are tricky to detect, but the fourth is rather easy to double-check: just search for all casts between integrals and pointers and make sure they all make sense.
    Except ... how do you find pointer casts? How do they differ from parenthesed expressions, from function declarations or function calls? In C, they don't: it's all a pair of parentheses. In C++, you know it's where the reinterpret_casts are. It's easy to search for.

    4) Casts are bad. Every cast is a potential design problem. Some are unavoidable, some are just fine, but every single one must be considered carefully. Hopefully, writing the full operator makes you think harder about using the cast. Typing a pair of parentheses is just too easy.

    5) Polymorphism. At the end of the day, the C-style cast simply cannot do what dynamic_cast does.


    Rule of thumb: just because the compiler allows it, doesn't mean it's a good thing. The compiler allows you to write a 10000 line main() littered with gotos jumping all over the place. Most compilers allow you to call main() explicitly, although such a thing is forbidden by the standard. Look at the entries of the Obfuscated C contest for a taste of all the stuff that the compiler allows that is a really bad idea.
    All the buzzt!
    CornedBee

    "There is not now, nor has there ever been, nor will there ever be, any programming language in which it is the least bit difficult to write bad code."
    - Flon's Law

Popular pages Recent additions subscribe to a feed