Click here to Skip to main content
15,881,092 members
Articles / All Topics
Technical Blog

Beware of Implicit Type Casting

Rate me:
Please Sign up or sign in to vote.
4.00/5 (1 vote)
26 Sep 2013CPOL3 min read 7.7K   1   3
Beware of implicit type casting

Type casting consists of converting an expression of a given type into another type. It can be done by explicitly telling the compiler which type the expression must be converted to, for instance:

C++
float x = 3.14;
int i = int(x);  // i is assigned to 3.
A* a = foo();
B* b = static_cast<B*>(a);
C* c = reinterpret_cast<C*>(b);

It can also be done implicitly by letting the compiler decide which type conversion is appropriate to successfully compile the source code. It follows type conversion rules, e.g., walk up the class hierarchy to find the implementation of a non-virtual method. This makes the code simpler to write and easier to read. However adding one’s own rules to the lot can lead to subtle bugs, whose cause is rooted in implicit type casting. I will illustrate with a simple example, inspired by a recent real case.

Let us say that you have a class A, and 100,000's lines of legacy code using pointers to A. The code looks something like this. Note how the code tests for NULL pointers (lines 17, 19, 21): pointers are implicitly converted to Boolean, which is perfectly correct.

C++
class A;
typedef A* ptra;

class A {
public:
  int foo(ptra a, ptra b);
  int bar();
  int operator[](int i);
};

int A::foo(ptra a, ptra b) {
  if (a && b) {
    return (*a)[b->bar()];
  } else if (a && !b) {
    return a->bar();
  } else if (b) {
    return b->bar();
  } else {
    return 0;
  }
}

Later, the behavior of the type ptra was extended (for instance, to add a reference count). A class wrapping ptra was added to avoid disrupting the existing code and public APIs. It looked like this:

C++
template <class T>
class Ptr {
public:
    Ptr() : p_(NULL) {}
    Ptr(T* p) : p_(p) {}

    T* get() const { return p_; }
    T* operator -> () const { return p_; }
    T& operator * () const { return *p_; }

    operator bool () const { return (p_ != NULL); }
private:
    T* p_;
};

typedef Ptr<A> ptra;

Note the type conversion of Ptr to bool defined in line 37. Thanks to it, the existing code that implicitly checks for NULL pointers does not need to be touched: the statement ‘if (a) {...}‘ will behave as before. However, the implicit type conversion has unexpected effects. Consider the following code:

C++
typedef Ptr<int> PtrInt;
typedef Ptr<char> PtrChar;

int main() {
    int i1;
    int i2;
    PtrInt a1(&i1);
    PtrInt a2(&i2);

    PtrChar b2((char*)&i2);

    assert(a1.get() != a2.get());
    if (a1 != a2) {
        cout << "OK: a1 != a2\n";
    } else {
        cout << "WRONG: a1 != a2 failed.\n";
    }

    if (b2 == a2) {
        cout << "OK: b2 == a2\n";
    } else {
        cout << "WRONG: b2 == a2 failed.\n";
    }

    if (b2 != a1) {
        cout << "OK: b2 != a1\n";
    } else {
        cout << "WRONG: b2 != a1 failed.\n";
    }

    return 0;
}

It produces the following output:

ocoudert:~/src$ g++ sample.cc && a.out
WRONG: a1 != a2 failed.
OK: b2 == a2
WRONG: b2 != a1 failed.

Quick, which output lines are correct? The answer is: none of them.

Line 61 and 67 are problematic: they compare two objects templated with two different classes. They should not even compile! Line 55 goes against the intuition: both a1 and a2 do wrap different pointers, as asserted by line 54.

In line 55, lacking an explicit Ptr::operator== definition, both a1 and a2 are implicitly converted to a bool. Because a1 and a2 are non null, they are seen as true values, resulting in the ‘else’ branch to be executed. Thanks to the implicit promotion to bool, line 61 and 67 compile properly, but the result of such comparisons is irrelevant.

To fix the problem on line 55, we need to explicitly define comparison on Ptr.

C++
template <class T>
bool operator == (const Ptr<T>& a, const Ptr<T>& b) { return a.get() == b.get(); }
template <class T>
bool operator != (const Ptr<T>& a, const Ptr<T>& b) { return a.get() != b.get(); }

This will produce:

ocoudert:~/src$ g++ sample.cc && a.out
OK: a1 != a2
OK: b2 == a2
WRONG: b2 != a1 failed.

For lines 61 and 67, we need to decide of the semantics when comparing two Ptr instantiated with different classes. Let’s say that the comparison should be on the type, not the pointer:

C++
template <class T, class U>
bool operator == (const Ptr<T>& a, const Ptr<U>& b) { return false; }
template <class T, class U>
bool operator != (const Ptr<T>& a, const Ptr<U>& b) { return true; }

We now have:

C++
ocoudert:~/src$ g++ sample.cc && a.out
OK: a1 != a2
WRONG: b2 == a2 failed.
OK: b2 != a1

If we choose to carry the comparison on the pointer itself, we would write:

C++
template <class T, class U>
bool operator == (const Ptr<T>& a, const Ptr<U>& b) { return a.get() == (T*)b.get(); }
template <class T, class U>
bool operator != (const Ptr<T>& a, const Ptr<U>& b) { return a.get() != (T*)b.get(); }

And we get instead:

ocoudert:~/src$ g++ sample.cc && a.out
OK: a1 != a2
OK: b2 == a2
OK: b2 != a1

However, we are still not out of the woods. We need to consider all the Boolean operators. For example, we can still write ((a1 < a2) || (a2 < a1)), and that expression will evaluate to false. The complete approach should follow the safe bool idiom.

The conclusion: implicit type casting makes the code easier to write and more legible; it is also a great tool to work with legacy code one cannot afford to change. But this must be done very carefully. Unwanted implicit type casting may create more problems than they solve.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect OC Consulting
United States United States
I have 20 years experience in software architecture and product development, including 10 years experience in research. I worked at eBay, Synopsys, Mentor Graphics, Magma, and I am an independent consultant in software design and development. I have published 50+ research papers or book chapters, and invented several algorithms for which I hold a few patents.

I am interested in technology as a whole, in particular software, hardware, and web-based applications. Check me out on LinkedIn or twitter (@ocoudert).

Comments and Discussions

 
GeneralThoughts Pin
PIEBALDconsult27-Sep-13 3:43
mvePIEBALDconsult27-Sep-13 3:43 
GeneralRe: Thoughts Pin
Axel Rietschin27-Sep-13 14:03
professionalAxel Rietschin27-Sep-13 14:03 
Uh? The compiler knows more about types than you do Smile | :) Type-casting really means to coerce a type into another, you don't really need to tell the compiler things it already knows... or did I miss something?

GeneralRe: Thoughts Pin
PIEBALDconsult27-Sep-13 14:39
mvePIEBALDconsult27-Sep-13 14:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.