Write Your Own
Eshaan Aggarwal @EshaanAgg
This is a collection of common C++ classes and templates that we often use, and thus are perfect opportunities for interviewers to quiz on to see if the candidate is comfortable with the language, can write clean code, and is aware about the internals of the language.
Smart Pointers
General Smart Pointer
This is the general implementation of a smart pointer that manages the memory of the pointer. The smart pointer is a class that wraps a raw pointer and manages the memory of the raw pointer. The smart pointer is responsible for deleting the memory when the object goes out of scope. The smart pointer is an application of the RAII (Resource Acquisition Is Initialization) idiom, where the resource is acquired in the constructor and released in the destructor.
While this class is a good starting point, it has some limitations:
- When passed to function by value, the pointer is moved to the function and the memory would be freed when the function returns. Thus in the original function, this can lead to a dangling pointer.
- This deletes using the non-array
delete
operator, and thus if the pointer was allocated usingnew[]
, it would lead to undefined behaviour.
Because of many more such reasons, the AutoPtr
class was first introduced in C++98
, but was later deprecated in C++11
. It was replaced by std::unique_ptr
, std::shared_ptr
, and std::weak_ptr
, which implement the move semantics and the copy semantics in a more robust way.
Unique Pointer
A std::unique_ptr
is a smart pointer that owns and manages another object through a pointer and disposes of that object when the std::unique_ptr
goes out of scope. The std::unique_ptr
is unique in the sense that it cannot be copied or assigned to another std::unique_ptr
, and thus there is only one owner of the object (the owner can be transferred using the move semantics).
Shared Pointer
A std::shared_ptr
is a smart pointer that retains shared ownership of an object through a pointer. Several std::shared_ptr
objects may own the same object. The object is destroyed and its memory deallocated when all std::shared_ptr
objects that own it are destroyed or reset. The std::shared_ptr
uses a reference count to keep track of the number of std::shared_ptr
objects that own the object.
Hit Counter
Base Variant
You need to implement a HitCounter
class that supports the following operations:
HitCounter()
Initializes the object of the class.void hit(int timestamp)
Records a hit that happened at the given timestamp.int getHits(int timestamp)
Returns the number of hits in the past 5 minutes.
Each function accepts a timestamp
parameter (in seconds granularity) and you may assume that calls are being made to the system in chronological order (i.e. the timestamp is monotonically increasing).
This function uses a deque
to store the hits, and a curCnt
to store the total number of hits. The removeOldHits
function is used to remove the hits that are older than 5 minutes. One salient feature of this implementation is that it does not store the hits for each second, but rather stores the number of hits at each second, and thus this implementation would gracefully handle the case when there are multiple hits at the same second. If the same was not a requirement, we could have used a queue
instead of a deque
to store the hits.
- Time Complexity:
hit
:getHits
:
Unordered Timestamps
Implement the same interface as above, but now the timestamps are not guaranteed to be in order.
-
Time Complexity:
hit
:getHits
:
where is the number of unique timestamps
Concurrent Hit Counter
Implement the same interface as above, but now the class needs to be thread-safe. You can assume that the threads would be calling the same in chronological order.
Infinite Timestamps in the Past
In this variant, we want the hit counter to be able to answer queries of all the time in the past, and the solution should use constant memory. Some inaccuracy is allowed in the answer. To handle the same, we would make use of buckets of size of powers of 2, so that the older buckets store more points and are updated less frequently.
Vector
The std::vector
is a dynamic array that can grow and shrink in size. It is a sequence container that encapsulates dynamic size arrays. The storage of the vector is handled automatically, being expanded and contracted as needed. Vectors usually occupy more space than static arrays, because more memory is allocated to handle future growth. This way a vector does not need to reallocate each time an element is inserted, but only when the additional memory is exhausted.
For our implementation, we set the capacity of the vector to be powers of 2, so that the reallocation is done less frequently (though other exponential growth factors can be used as well). We will make use of templates to make the vector generic, and try to provide the same interface as the std::vector
. We will also provide the operator[]
to access the elements of the vector, and implement iterators to traverse the vector.
Note that in the following implementation, we have used std:::move
to move the elements of the vector so that we can avoid copying the elements. To also provide support for emplace_back
, we have used the Args &&...args
to forward the arguments to the constructor of the object, and thus the template class needs an additional template parameter parameter Args
which is vardiadic.
Semaphore
A semaphore is a synchronization primitive that is used to control access to a shared resource. It is a variable that is used to control access to a shared resource by multiple processes in a concurrent system such that the resource is not used by more than a certain number of process at a time. The semaphore has two operations, acquire
and release
, which are also known as P
and V
operations respectively. The acquire
function waits until the resource is available, and then decrements the available count. The release
function increments the available count and notifies one of the waiting threads.
With Condition Variables
This implementation uses a std::mutex
and a std::condition_variable
to implement the semaphore. A condition variable allows a thread to be signaled when something of interest to that thread occurs, which is useful to avoid busy waiting. Note the use of std::unique_lock
to lock the mutex, instead of the usual std::lock_guard
, as the std::condition_variable
requires the mutex to be locked when calling wait
.
Without Condition Variables
Since we are not using condition variables, we need to busy wait until the resource is available. This is not a good practice as it wastes CPU cycles, but it is useful to understand the concept of semaphores. We now make use of std::atomic
to make the operations atomic, and a std::mutex
to protect the critical section.
Producer Consumer Problem
The infamous Producer-Consumer problem, also called the Bounded-Buffer problem, is one of the famous real-world scenarios of synchronization. The problem describes two processes, the producer and the consumer, who share a common, fixed-size buffer used as a queue. The producer’s job is to generate data, put it into the buffer, and start again. At the same time, the consumer is consuming the data (i.e., removing it from the buffer), one piece at a time. The problem is to make sure that the producer won’t try to add data into the buffer if it’s full and that the consumer won’t try to remove data from an empty buffer.
We make use of the following classes to implement the producer-consumer problem:
std::mutex
: We use a mutex to protect the critical section, i.e., the buffer.std::condition_variable
: We use a condition variable to notify the consumer when the buffer is not empty and the producer when the buffer is not full.std::queue
: We use a queue to store the data.
In the main function, we create two threads, one for the producer and one for the consumer. The producer produces data and pushes it into the buffer, and the consumer consumes the data by popping it from the buffer. The producer waits if the buffer is full, and the consumer waits if the buffer is empty.
Singleton Design Pattern
A singleton class is a class such that you can only have one instance of the class. This is useful when you want to have a single instance of a class that is shared across the application. The singleton class is implemented using a static member variable, and a static member function that returns the instance of the class.
Singletons are useful when we want to apply some functionalities to a “global” set of data, but we do not want to pass the object or the data/member functions around. Having the same encapsulated in a singleton class makes it easier to manage and use throughout the application. They can effectively be managed using namespaces as well, but the singleton class provides a more object-oriented approach.
Here are some good examples of a singleton class:
- Logger
- Configuration
- Random Number Generator
- Database Connection
If you want to call the member functions directly on the class, you can make the member functions static as well with a bit of indirection.