The Quest For A Perfect C++ Interview Question
Is there such a thing as a perfect interview question? Is there a magic silver bullet that enables you to hire star performers and call an end to all your engineering woes?
I don't think so. Or that is the conclusion I've reached every time I debated and searched the answer to this question with my friends and colleagues.
However, you can come pretty darn close in some situations. If you're looking for a candidate with a specific skillset in any programming language, crafting a near optimal interview question is probably not as far-fetched as it is if you're trying to assess the general aptitude and thinking abilities of a candidate.
Before we even get started, let me assert that I'm not a big fan of asking knowledge based questions or questions that require intimate knowledge of specific programming languages in interviews. I prefer hiring for the potential as assessed by system design discussion or tough algorithmic challenges. At Microsoft, we tend to give new hires between 3-6 months before expecting anything productive from them. Not every company can afford such a luxury.
Having said that, if your project is under fire and you need a developer with good C++ knowledge right now, asking them to implement a smart object that manages its own heap memory is probably as comprehensive as you can get.
Which C++ concepts does implementing a smart pointer test?
This question pretty much touches on all the concepts necessary to write and debug C++ code at production level. Concepts tested implicitly include:
- Pointers and References
- Stack vs Heap memory
- C++ Templates
- Ref counting as a way of managing object lifetime.
- Copy constructors
- Operator overloading
- Deep vs Shallow copy
The whole solution is based on the fact that the smart pointer is allocated on the stack and the destructor for it is automatically called when the pointer goes out of scope. If there are multiple smart pointers pointing to the same object, the reference count is decremented each time a pointer goes out of scope and when the last pointer goes out of scope, the underlying object is deleted.
Crafting the Smart Pointer one step at a time
Step 1: Create a simple class to keep track of how many smart pointers are pointing to the object.
The class object has a m_Count variable which is incremented each time a new smart pointer is created, copied or assigned and decremented when a smart pointer stops pointing to the object or is deleted.
/* Reference Count: A simple class for managing the number of active smart pointers*/ class ReferenceCount { private: int m_RefCount{ 0 }; public: void Increment() { ++m_RefCount; } int Decrement() { return --m_RefCount; } int GetCount() const { return m_RefCount; } };
Step 2: Create the Smart Pointer template class.
The smart pointer class holds a pointer to the underlying object and a pointer to the reference counter object. This is such that the reference count object can be shared among different smart pointers pointing to the same underlying object.
template <typename T> class smart_ptr { private: T* m_Object{ nullptr }; ReferenceCount* m_ReferenceCount{ nullptr }; public: smart_ptr() { }
Step 3: Create the Smart Pointer constructor and destructor
The m_Object is initialized to the underlying object in the constructor. The constructor also creates a new ReferenceCount object that can be shared by different instances of the smart pointer.
Inside the constructor, because we just created a smart pointer object, we increment the ref counter.
In a similar way, the destructor decrements the ref count when the smart pointer is destroyed. Additionally, if this is the last smart pointer which is being destroyed, it destroys the underlying physical object and the Reference counter by calling delete on them.
//Constructor smart_ptr(T* object) : m_Object{ object } , m_ReferenceCount{ new ReferenceCount() } { m_ReferenceCount->Increment(); cout << "Created smart_ptr! Ref count is " << m_ReferenceCount->GetCount() << endl; } //Destructor virtual ~smart_ptr() { if (m_ReferenceCount) { int decrementedCount = m_ReferenceCount->Decrement(); cout << "Destroyed smart_ptr! Ref count is " << decrementedCount << endl; if (decrementedCount <= 0) { delete m_ReferenceCount; delete m_Object; m_ReferenceCount = nullptr; m_Object = nullptr; } } }
Step 4: Provide a copy constructor and overloaded assignment operator
Notice that there is a marked difference between the copy constructor and the overloaded assignment operator. Remember this for the interview!
In case of the copy constructor, the object from which we’re copying is not modified – so the only thing we need to do is:
- Copy the pointer to the underlying object
- Copy the pointer to the Ref count object
- Increment the Ref count
However, in the case of the assignment operator, we also need to make sure that we decrement the ref count of the current object being pointed at before reassigning. Also if the pointer being assigned to is the only smart pointer holding a reference to its underlying object, we need to delete the object and associated reference counter.
Once we’ve done the above housekeeping, we can follow the same logic as that of the copy constructor.
// Copy Constructor smart_ptr(const smart_ptr<T>& other) : m_Object{ other.m_Object } , m_ReferenceCount{ other.m_ReferenceCount } { m_ReferenceCount->Increment(); cout << "Copied smart_ptr! Ref count is " << m_ReferenceCount->GetCount() << endl; } // Overloaded Assignment Operator smart_ptr<T>& operator=(const smart_ptr<T>& other) { if (this != &other) { if (m_ReferenceCount && m_ReferenceCount->Decrement() == 0) { delete m_ReferenceCount; delete m_Object; } m_Object = other.m_Object; m_ReferenceCount = other.m_ReferenceCount; m_ReferenceCount->Increment(); } cout << "Assigning smart_ptr! Ref count is " << m_ReferenceCount->GetCount() << endl; return *this; }
Step 5: Provide an overload for the Dereference operator and Member access operator
This is a crucial step because it provides you the ability to use a smart pointer like a regular pointer.
//Dereference operator T& operator*() { return *m_Object; } //Member Access operator T* operator->() { return m_Object; }
And that’s it ! Now just write a small driver program to test your code like the one below:
class AirCraft { private: std::string m_Model; public: AirCraft() :m_Model("Generic Model") { cout << "Generic model aircraft created" << endl; } AirCraft(const string& modelName) :m_Model(modelName) { cout << "Aircraft type" << m_Model << "is created!" << endl; } void SetAirCraftModel(const string& modelName) { cout << "Aircraft model changed from " << m_Model << " to " << modelName << endl; m_Model = modelName; } ~AirCraft() { cout << "Destroying Aircraft of model:" << m_Model << "!" << endl; } }; int main() { // Create two aircraft objects. smart_ptr<AirCraft> raptorPointer(new AirCraft("F-22 Raptor")); // Ref Count for raptorPointer = 1 raptorPointer->SetAirCraftModel("B2 Bomber"); // rename the model using pointer access operator (*raptorPointer).SetAirCraftModel("B2 Spirit"); // rename the model using the pointer dereference operator smart_ptr<AirCraft> hornettPointer(new AirCraft("F-14 Hornett")); // Ref count for hornettPointer = 1 raptorPointer = hornettPointer; // raptorPointer now points to "F14-Hornett".Ref count for hornett is 2. "F-22 Raptor" is destroyed. Ref count for hornett is 2 return 0; }
The output of the program above is in line with our expectations:
Aircraft typeF-22 Raptor is created! Created smart_ptr! Ref count is 1 Aircraft model changed from F-22 Raptor to B2 Bomber Aircraft model changed from B2 Bomber to B2 Spirit Aircraft typeF-14 Hornettis created! Created smart_ptr! Ref count is 1 Destroying Aircraft of model:B2 Spirit! Assigning smart_ptr! Ref count is 2 Destroyed smart_ptr! Ref count is 1 Destroyed smart_ptr! Ref count is 0 Destroying Aircraft of model:F-14 Hornett!
The full code listing can be found here: Implementing A Smart Pointer Using Reference Counting
So what am I missing?
This implementation is fit for interview and educational purposes only. It barely scratches the surface in terms of all the things the modern C++ 11 or Boost libraries provides.
However, if an interview candidate were able to chalk out this solution, it opens up the possibility of having a fantastic discussion around the limitations of this solution. An interviewer can get significant insight about the candidate’s C++ knowledge while discussing the limitation.
There are probably a number of mistakes and room for optimizations to this code.
I’ll start with the critique list:
- Reference counter class is not thread safe. Consider using synchronization primitives when incrementing/decrementing ref counts.
- Missing move constructor and move assignment operator
- No way to pass custom deleters in the constructor – how will you manage Array type objects ?
- No Reset() functionality – which is needed for a very useful Reset() based initialization and Destruction pattern.
Please feel free to critique this code in comments section and add to the list !!!
Finally…
If you're interested in learning more about the nuances of smart pointers, I'd recommend the following books. Both are pre C++ 11 and have sections devoted to
- Modern C++ Design: Generic Programming and Design Patterns Applied by Andrei Alexandrescu
- More Effective C++: 35 New Ways to Improve Your Programs and Designs by Scott Meyers
What do you think about asking this question in an interview ? Do you think it's an effective way to gauge someone's C++ prowess ? Do you think it's a fair interview question ?
If you enjoyed this post, I’d be very grateful if you’d help it spread by sharing it with your friends and colleagues. Thank you! 🙂