Common C++ Gotchas

Hopping between my open-source Android projects and Photoshop Elements at Adobe involves a lot of mental context-switching. A majority of that transition is necessitated by the fact that Java and C++ share similar syntax and semantics but differ in countless ways, often subtly. I often catch myself "thinking in Java" when writing C++, or the other way around, when I'm in the midst of said context-switch.

The other thing to consider is: C++ is a double-edged sword. It's powerful, but that also makes it very easy to make mistakes.

WTFs per minute

So I thought I'd compile a list of common C++ pitfalls / things to remember / things I find interesting, with references to more detailed discussions. I expect to often refer to this post myself. The format is somewhat terse, so beginners might find it confusing.

1. delete heap memory (or just use smart pointers)

I know you don't need me to tell you this. But sometimes I forget to call delete (or delete[]), so this item is basically a reminder for myself. It doesn't help that accidentally calling vanilla delete on new[]'ed memory causes undefined behaviour, potentially including subtle memory leaks.

Or, as many commenters rightly pointed out, you could simply use smart pointers like C++11's std::unique_ptr and std::shared_ptr and side-step this whole mess altogether.

2. never throw from a destructor

Yes, I hear you: "But what if my destructor fails?". Simmer down, whippersnapper. I'm calling in the cavalry on this one. Here's Marshall Cline's suggestion (Cline is author of the treasured C++ FAQ):

Write a message to a log-file. Or call Aunt Tilda. But do not throw an exception!

You at least trust him, if not me, right? You should. Here's the reason: when an exception is being processed, the program begins moving up the call stack in search of a catch block, popping off stack frames and destroying all local objects in them. Guess what happens if the destructor of one those objects throws an exception? Now there are two uncaught exceptions to deal with, and no catch in sight! At this point the program simply explodes in your face, as the realization dawns that you've committed thrown one sin exception too many in the Holy Land of C++.

3. declare base class destructors as virtual

class A {  
    ~A() { ... }
};

class B : public A {  
    ~B() { ... }
};

A *a = new B();  
delete a;  // what happens here?  

The call to delete at the end will, in most cases, call A::~A(), resulting in a hard-to-detect memory leak. The standard's stance on this issue is "undefined behaviour". Making A's destructor virtual resolves the leak.

4. always catch references, not values or pointers

Spot the best code:

// option 1
try { ... }  
catch (exception e) { ... }

// option 2
try { ... }  
catch (exception& e) { ... }

// option 3
try { ... }  
catch (exception* e) { ... }  

Yes, yes, it's option 2. But why?

Option 1 is bad because: (a) it might involve an unnecessary copy constructor invocation, and (b) it can cause a subtle bug, thanks to slicing! For example, if the try block throws an instance of type derived_exception, derived from the type exception (duh), then the latter's copy constructor gets called, resulting in a hacked-off instance of the former getting sliced into the latter.

Option 3 is bad simply because naked pointers need to be manually de-allocated. This is especially bad considering the fact that when I'm writing a catch block, I already have a lot to think about: how to recover from the error, whether I can punt responsibility up the call stack, and so on. So no naked pointers to exceptions, thank you very much.

5. char const* const buffer

const follows a rather simple rule: it acts on whatever precedes it. So:

char const* buffer;        // pointer to const char (1)  
char* const buffer;        // const pointer to char (2)  
char const* const buffer;  // const pointer to const char (3)  

Much of the confusion arises because many (including me) tend to prefer placing the const before the char in the first case:

const char* buffer;        // pointer to const char, same as (1)  

If you'll tolerate my hypocrisy for a moment, here's my suggestion: try to avoid putting the const at the beginning like that. Another source of confusion is array declarations with const:

int main(int argc, char* const* argv);   // pointer to const pointer to char  
int main(int argc, char* const argv[]);  // equivalent  

6. copy constructor signature

What's the right signature for a copy constructor?

  • A::A(A) - infinite recursion
  • A::A(A&) - valid but won't work if the argument is a temporary, e.g., A fn() { A a1; return a1; }; A a2 = fn(); // error
  • A::A(const A&) - valid and works with temporaries (see item #8)

7. return value of operator=

Consider the overridden assignment operator for class A (our favourite class):

??? A::operator=(const A& other) {   // guess the return type?
   // assign other to this
   // ...

   return ???;   // what to return here?
}

The standard says the return type of operator= must be A& and the return value "a reference to the left hand operand", i.e., *this. Without a satisfactory reason, that's just begging to be forgotten. Here's your reason: it's so that chained assignments like a = b = c work correctly (think of it as a.operator=(b.operator=(c))).

8. temporary objects can only bind to const references

A getA() { return A(); }  
const A &a = getA();  // ok  
A &a = getA();        // error  

You cannot assign temporaries to non-const references. You knew that right? Yes, I'm looking at you, Visual Studio 2010 user. Your compiler doesn't even emit a measly warning for decidedly non-standard behaviour. Clang and GCC are kind enough to spit out descriptive compilation errors. This has bitten me more than once, when writing cross-platform code in Visual Studio.

9. the parentheses in new A() matter more than you think they do

class A { int n; };  
A a1 = new A();  
A a2 = new A;  

What do you think are the values of a1.n and a2.n right after construction? Answer: 0 and {insert garbage value here}. More generally, plain old datatypes are zero-initialized in the former case, but behaviour in the latter case is implementation-defined.

10. dynamic_cast works only with runtime polymorphic types

Sure, your compiler helps you out with this. But this is very easy to forget, say, in an interview. So have you thought about why at least one virtual function is needed for using dynamic_cast? Simply because it's convenient to implement dynamic_cast by piggy-backing on the virtual method infrastructure: the type hierarchy information (conventionally called "run-time type information" or "RTTI") is usually attached to the vtable, and a run-time check is performed, using available RTTI, to determine whether the cast is valid.


I know I bossed you around a bit throughout the post, dear reader. But hey, good news! Vent all you want in the comments, especially if you think I'm downright wrong somewhere above.

comments powered by Disqus