You are not protected by the compiler

Dynamic vs static typing debates are often passionate. Probably too passionate to be totally objective. During one of these discussions I was involved in, a well respected senior engineer have made an interesting statement :

” … My lisp program crashed after hours of computation right before delivering the result due to a missing method runtime error. This would’ve NEVER happened in a statically typed language. For instance, this would’ve never happened in C++ using the latest Microsoft C++ compiler. With all the static analysis it performs, these errors are prevented. You are protected by the compiler! …”

Well, I don’t know how good is the MS compiler compared to other C++ compilers. Still, we’ll show how ridiculously easy it is to fool it and have the same missing method crash.


We will be using Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80×86, with “enable all warnings” and “treat warnings as errors” flags activated. We will also activate the “Enable Code analysis for C/C++ on Build” option in the “Code Analysis” project properties section and we will use “Microsoft All Rules” as a rule set.
In the code samples below, we will stick to ISO C++ with no third party libraries usage. Not even the standard C++ library.

Case 1 : Dispatch table corruption

Every C++ developer has run into this problem at least once in his life. An object gets deleted but the program keeps, somehow, a reference to it. Everything goes fine until you try to call a virtual method on this object : Boom! You’re most likely going to have a memory protection error followed by a crash. Of course, this will not happen during development or QA testing. This will happen in production when your application is most needed :) .

Here is a snippet that illustrates the problem.

struct Base
{
    virtual void Do() =0;
};

struct Derived : public Base
{
    virtual void Do() {}
};

void main()
{
    Derived *p = new Derived();
    delete p;
    p->Do();
}

Derived class overrides and implements an abstract pure virtual method defined in class Base. In main(), we create a Derived object on the heap; we delete it and then we try calling the virtual method. The compiler compiles this program with no single warning. Static analysis detects nothing either. However, at runtime, it’s a different story. The dispatch table used for method resolution at runtime is not valid anymore after the object is delete. Calling the virtual method will amount to accessing a memory space that we’ve just disposed; hence a runtime error.

Memory corruption is unfortunately quite common in C++ and the compiler just can’t anything about it. This is why C++ developers use tools like valgrind and purify. They also use smart pointers and/or disciplined memory usage patterns to help containing these problems. You just can’t rely on the compiler.

Case 2 : Virtual method call from constructor

Memory corruption is not mandatory to have our missing method runtime error. Let’s have a look at the following snippet:

struct Base
{
    Base() { Do(); }
    void Do() { ReallyDo(); }
    virtual void ReallyDo() =0;
};

struct Derived : public Base
{
    virtual void ReallyDo() {}
};

void main()
{
    Derived d;
}

This program will crash complaining about a “pure virtual function call”. To understand what’s happening here, we must recall that objects are constructed top down in C++: from ancestors down to children classes. To construct the object d in main(), Base is built first. Base constructor is called, it calls method Do() that calls ReallyDo(). At this point, Derived is not built yet and thus the virtual table doesn’t contain yet the address to the correct virtual method. Therefore, the call to ReallyDo() fails.

To sum up,  the program is apparently calling an abstract (not implemented) method and, again, the compiler and the static analysis engine didn’t see it coming.

But wait, does this error really happen in real life? We’ve only provided toy programs to reproduce it after all. Well, the answer is Yes, it does happen even in widely used and heavily tested applications. Here is a screenshot of a crash I had recently using Adobe Acrobat Reader.

Conclusion

The compiler cannot help you write better code or avoid bugs. This is an urban legend that, unfortunately, some developers and managers keep propagating. Probably because of the false sense of security that a “Build succeeded” compiler message gives. If you are a manager and you want your team to write better quality code, try to invest a bit more on testing, code reviews, setting up best practices etc. You are Not protected by the compiler.

The You are not protected by the compiler by Chaker Nakhli, unless otherwise expressly stated, is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License.

17 Comments

  1. [...] This post was mentioned on Twitter by Bernard Notarianni, Chaker Nakhli. Chaker Nakhli said: You are protected by the compiler! (Or you might think so) http://j.mp/gJyWEP #cplusplus #troll [...]

  2. Hugo says:

    Very interesting post.

  3. Good post. You mention testing as a way to improve quality. If test is only done after development, your process will create bugs. Many quality gurus tell us we cannot test in quality with tests. This is true for tests conceived and executed after the code is developed. An approach to prevent defects is Test Driven Development. In TDD, production code is written incrementally in response to a failing test. Here is a link to an article that compares Debug Later Programming to Test Driven Development http://www.renaissancesoftware.net/blog/archives/16.

    Code reviews are a great way to eliminate bugs too, but there are limitations. The famous Zune bug illustrates the fallibility of reviews, and how you must run code to know for sure how it works. Here’s article on that: http://www.renaissancesoftware.net/blog/archives/38

    James

  4. Jason says:

    It’s true that the compiler can’t protect you from creating bugs. But I find that it most definitely does protect you from certain types of errors, particularly typos and misspellings. A missing method, or a mis-typed method call would definitely be prevented by the compiler so the senior engineer was exactly correct to say that. Obviously I have no idea if he was suggesting that the compiler will prevent all bugs. I can’t imagine anybody with any amount of experience who would think that.

  5. Paulo Pinto says:

    You have picked the wrong language as an example of static typing. All of those examples are actually C++ issues.

    The first issue is usually not possible in GC enabled static languages.

    The second issue again, has to do with C++ object construction, which in other OO static languages wouldn’t be an issue.

    What about providing similar examples in other static languages, more sane than C++?

  6. Achilleas Margaritis says:

    You compare apples to oranges. In the Lisp case, the missing method was the bug, whereas in the case of C++, the missing method was because of a bug.

  7. @Achilleas Margaritis: Indeed, you are absolutely right. What I tried to show though, is that run-time crashes due to easy-to-make common mistakes do not only happen when you use dynamic languages. In C++ also you can inadvertently double delete a pointer, reuse a deleted pointer or call a virtual method in a constructor (like Adobe developers did in Acrobat Reader).

  8. @Paulo Pinto: Indeed, in modern languages you won’t have a crash when you call a virtual method from the constructor. As opposed to C++, the dispatch table will be valid before your virtual method gets called. However, the most derived classes constructors code is not run yet when your method is called. You will run the virtual method in an not initialized object. You’ll probably manipulate a corrupt state.

    Example (c#):
    public abstract class Base
    {
    protected int i = 2;
    protected Base() { Print(); }
    public abstract void Print();
    }

    public class BaseImpl : Base
    {
    public BaseImpl() { i = 5; }
    // you are expecting the output to be 5, right? well it is not...
    public override void Print() { Console.WriteLine(i); }
    }

  9. sbi says:

    So you picked a two examples where a statically checking compiler couldn’t help, and from that “conclude” that compilers cannot help you?

    My analytical skills must be way below yours, because Ii can’t follow your deductions.

  10. Anonymous says:

    Some languages with really, *really* strong type systems (like Haskell) are kind of interesting because they really do prevent many, many bugs. It is difficult to trick such languages without resorting to unsafe functions. You can still mess things up in your logic, but nothing can prevent that.

  11. @sbi technically, these are counterexamples for the claim “static languages are safer/less error prone than dynamic languages“. So yes, a counterexample (mathematically and logically) can be a valid demonstration to invalidate a false claim. Sorry if I was not able to comply with your analytical skills. If you have other objections please keep posting them here.

  12. @Anonymous it can be indeed very helpful. However, everything is a matter of picking the right tool for your need. For some projects, duck typing –with the conciseness and flexibility it gives– is more than enough. For other projects/teams Haskell, with its strong type system and pure functional style, is more appropriate. This article’s point is: static typing is not better or worse than dynamic typing. Pick the tool you need for the task/context you have.

  13. sbi says:

    @Chaker A counter example can invalidate an _absolute_ claim, but not a relative one. But nobody in their right minds says static checking prevents _all_ errors, so showing off errors that are not prevented does not invalidate the claim that static checking prevents more errors.

    Frankly, I feel a bit silly for having to explain this to a programmer.

  14. @sbi then you missed the point of this article: this article is here to invalidate the absolute claim, nothing more. My PhD is partly about static analysis, I do know how helpful static analysis can be. What you missed here is the following: static analysis and static typing is no silver bullet.

    And please don’t feel silly :)

  15. DeadMG says:

    Your examples do not prove that in the slightest. All you’ve demonstrated is that T* is an unsafe type, which isn’t news to anybody. If you had used ownership-based management like a sane person, then static analysis would have prevented case #1. And case #2, I personally am surprised that compilers do not catch it, but I also do not see why they could not.

    You need examples that are much less “The programmer was an idiot for not using the features of static typing”.

  16. @DeadMG Not sure why T* in particular is an unsafe type. Are trying to say that T* is unsafer than, say T&? What do you mean by “T* is a unsafe type”?
    Please notice that the second example about virtual method call in constructor does not involve any T* types.

  17. sbi says:

    @Chaker: In the comment up there your wrote:

    > technically, these are counterexamples for the claim “static languages are safer/less error prone than dynamic languages“.

    Note the “safer/less error prone.” That is not an absolute claim you were attacking there, but a relative claim. And you made this in defense against my initial statement that providing two counterexamples does not disprove that statically typed languages are safer.

    The statement you used as an excuse to write this blog posting speaks of an “missing method runtime error.” and continues to claim “This would’ve NEVER happened in a statically typed language.” And, of course, in a statically typed language a missing method is caught at compile time, so that statement is indeed true.

    Now, in your first example, you access a deleted object. This invokes Undefined Behavior, which might indeed crash, but that has nothing to do with virtual, or even non-virtual, methods. Accessing a (public) data field in a deceased object would result in UB, too. For what it’s worth, if I remember correctly (I am not a language lawyer) according to the C++ standard even just dereferencing an invalid pointer leads to UB.

    The fact that C++ does not protect you against such errors is well-known. It is one of the language’s design decisions that it trades safety for speed. On the other hand, with RAII C++ offers semi-automatic, deterministic resource management, that offers safety and speed at the same time. It would be foolish to not to use this. Directly using “delete” into your code has been considered wrong for at least a decade.

    As for your second example: I am with @DeadMG on that. Any decently modern compiler will give a diagnostic for that, although I suppose the C++ standard does not require one.

Leave a Reply