2. Cpp Interview Questions - thealexcons/low-latency-systems-notes GitHub Wiki
Test your C++ knowledge with some must-know interview questions about the language and its features.
Consider the following code:
class Test {
};
Test t;
What is sizeof(t)
?
Click to see the answer
At least 1. The compiler will add a dummy byte(s) because if the size is 0, two
distinct objects of class Test
will have the same address which is illegal:
Test t1, t2; // both stack objects will have the same address if their size is 0
Consider the following code:
const char* p = "Test";
What is the output of the expression *p++
?
Click to see the answer
The value is 'T'
. Due to operator precedence, the expression evaluates as *(p++)
. The postfix increment returns the old value, so it reduces to *p
, which is the first character of the string.
Consider the following code:
std::map<const char*, int> string_map;
What are your thoughts on the above code?
Click to see the answer
The map is using a char*
as the key, so the default comparison function compares two character pointers instead of the contents of the string. So, two map items will be the same only if the exact same pointer is used. If the intent is that the string contents are to be the keys, then you need to define a string comparison function, as below:
struct StrCmp {
bool operator()(char const *a, char const *b) const
{
return std::strcmp(a, b) < 0;
}
};
//StrCmp specified in map declaration
std::map<const char *, int, StrCmp> strMap;
Consider the following code:
class Test {
public:
virtual ~Test() {}
};
Test t;
What is sizeof(t)
?
Click to see the answer
It will be the size of a pointer (8 bytes on a 64bit machine). The reason is that the destructor is declared virtual so the compiler will create a virtual table pointer in all objects of class Test
.
What is the time complexity of accessing an element by index in std::deque
?
Click to see the answer
It is constant time. A deque is implemented as a (dynamic) array of pointers to fixed size chunks (arrays). Using the knowledge of the size of the chunks, it is constant time to determine what chunk/array contains the item, and then constant time to find the item within the chunk. This always involves two memory accesses.
Consider the following code:
int *p = new int[10];
p++;
delete[] p;
Is the above code safe?
Click to see the answer
No, this code has undefined behaviour. The second line will increment the address by 1 (4 bytes), so we are then deleting the address at (p+1) which was not created by new[]
.
Consider the following code:
class Test {
public:
static int x;
char c;
};
int Test::x = 3;
Test t;
What is sizeof(t)
?
Click to see the answer
It will be 1 (the size of a char) because static data members don't contribute to the size of an object.
Consider the following code:
class Test {
public:
Test(const Test& obj) {}
};
Test t;
Does the above code compile?
Click to see the answer
No, it won't compile. We have a user-defined copy constructor, so the compiler will not generate a default constructor required for the last statement (see rule of 5).
Consider the following code:
void f(std::string& s) {}
f("test");
Does the above code compile?
Click to see the answer
No, it won't compile. We are passing an rvalue (string literal) into the function, but in C++ a non-const reference cannot bind to an rvalue (temporary object). We can fix this by changing the parameter to void f(const std::string& s)
or void f(std::string&& s)
, since they both bind to rvalues.
Why do copy constructors take in a const reference in C++?
Click to see the answer
const
is wanted because you don't want to change the object from which the copy is made. Consider if we passed the other object by value: this will create a copy, which invokes the copy constructor again, leading to infinite recursion. Hence, we must pass in the object by reference.
Can destructors throw exceptions?
Click to see the answer
Technically yes. But it is dangerous to do so. Because if stack unwinding is in progress for a previous exception and then an object’s destructor throws another exception, its ambiguous which exception the C++ runtime should handle from a safety perspective, so the standard says to abort the program instead.
Can constructors throw exceptions?
Click to see the answer
Yes. In fact, throwing an exception from a constructor is good for two reasons:
- Constructors don't have a return value to signal failure
- There will be no memory leak, unless the constructor itself is allocating memory (can be solved by using smart pointers to avoid memory leaks).
Consider the following code:
class Base {
public:
virtual void f() { cout << "Base::f()" << endl; }
};
class Derived : public Base {
private:
void f() { cout << "Derived::f()" << endl; }
};
int main() {
Base * p = new Derived();
p->f();
}
Will the code compile? If so, what is the output?
Click to see the answer
Yes, it compiles and the output is "Derived::f()". Although Derived::f()
is private, since p
is a pointer of type Base
and this class has a function f()
, the type-checking passes. Since Base::f()
is virtual, dynamic dispatch at runtime will find the actual function from the derived class.
What is the placement new operator? How is it useful?
Click to see the answer
The placement new operator allows you to specify at what address a new object should be allocated. This is useful when we need to create multiple objects of the same type during execution but don't want to dynamically allocate each one of them. We can instead allocate a chunk of memory at the start and then use the placement new operator to directly create these objects in the pre-allocated memory slots. Also, once an object is destroyed, we can use the memory vacated to construct other objects. This is different to the regular new because it does not allocate memory, it simply constructs the object on already-allocated memory, whereas the regular new will allocate and initialise the memory with that type.
class Test {};
// pre-allocated buffer for 10 Test objects
char *buf = new char[10*sizeof(Test)];
// using placement new to place object directly in pre-allocated memory
Test *tp = new (buf) Test();
Explain what is a policy class with an example.
Click to see the answer
Policy classes are used to inject behaviour into some other class. These are commonly found in STL containers to specify allocators, comparators, etc. in order to dictate how a specific part of the container should behave. These are passed in typically through templates, for example:
template<class T, class Allocator = std::allocator<T>>
class vector { ... }
Explain copy elision with an example.
Click to see the answer
Copy elision is a compiler optimisation that eliminates unnecessary copying or moving of objects. An example of when copy elision may be useful is shown below:
class Test {
public:
Test() {}
~Test() {}
Test(const Test&) {}
};
Test foo() {
return Test();
}
int main() {
Test t = foo(); // line X
}
In line X, if copy elision is applied, the returned object is directly constructed in t
and no copy constructor is invoked. From C++17, copy elision is guaranteed for cases like the one above.
Consider the following code:
class Test {
int y, x;
public:
Test(int i) : x(i), y(x + 1) {
}
};
Is the above code safe?
Click to see the answer
No. When using the initialiser list in the constructor, the data members will be initialised in the order in which they are declared in the class and NOT in the order in which they appear in the initialiser list. Hence, y
will be initialised to x+1
but x
will not have been initialised, causing undefined behaviour.
Consider the following code:
template<class T> void foo() {
T::f *x;
}
struct X {
typedef int f;
};
int main() {
foo<X>();
return 0;
}
The code above does not compile. How can it be fixed?
Click to see the answer
In the template function foo
, we try to use a dependent name T::f
as a type (as an int pointer) for the variable x
(since it depends on the template parameter T). However, if we call foo<Y>()
with Y defined as struct Y { static int f = 23; };
, we are declaring a multiplication expression inside foo
(ie: 23 * x
). To avoid this ambiguity, C++ requires you to explicitly state the intent when a dependent name is to be treated as a type. The function should look like:
template<class T> void foo() {
typename T::f *x; // treat T::f as a type
}
Consider the following code:
template<typename T, typename U>
void foo() {} // case 1
template<typename T>
void foo<char, T>() {} // case 2
In the above code, is case 2 a valid specialisation of the function template in case 1?
Click to see the answer
No because function templates cannot be partially specialised in C++. They can be not specialised at all (in this case, 0 out of 2 template arguments) or fully specialised (both T and U).
Consider the following code:
class Base
{
// some virtual methods
};
class Derived : public Base
{
~Derived()
{
// Do some important cleanup
}
};
Base *b = new Derived();
// use b
delete b;
What's wrong with the code above?
Click to see the answer
Deleting b
is undefined behaviour because it points to a Derived
object but class Base
has no virtual destructor, so it cannot
find the overriden constructor in Derived
. To solve this, Base
should have a virtual destructor. In summary, always make base classes' destructors virtual
when they're meant to be manipulated polymorphically (which always happens through pointers).
Alternatively, if you want to prevent the deletion of an instance through a base class pointer, you can make the base class destructor protected and nonvirtual; by doing so, the compiler won't let you call delete
on a base class pointer.
Here is a record of all C++ related interview questions I have received (only includes questions about the language and the STL):
- What is
std::move
and what does it do? - What is the rule of zero/three/five?
- What is the RAII pattern?
- What is the difference between an rvalue reference and a universal reference?
- Explain what a "const reference" is to a C programmer.
- What does the
inline
keyword do and when is it necessary? - What is the difference between a static function vs a function in an anonymous namespace (if any)?
- When should you use
std::unique_ptr
overstd::shared_ptr
? - What is the difference from a performance and safety perspective of using
std::make_shared<T>()
vsstd::shared_ptr<T> foo(new T())
? - Explain how you can still leak memory when using
std::shared_ptr
? - What is the allocator API?
- What is the difference between new and placement new?
- How is
std::string
implemented? - What is stored in a vtable? (array of pointers to most specific method + in some impls, RTTI for the object if enabled)
- How are vtables implemented and what might their memory layout look like in a typical implementation?
- Explain in detail what happens when a virtual function is called through a pointer to a base class object?
- What is object slicing?
- What is the diamond problem and how would you get around it?
- What are the four categories of exception guarantees?
- What happens if you throw an exception in the destructor?
- What are stable/unstable containers?
- How is
std::unordered_map
implemented? What happens when there are too many elements? - What is the difference between
std::array
andstd::vector
? Which is faster if we want to access one element (randomly and ignoring caching)? - How does the reallocation process work in
std::vector
? (important to mention the move constructor must be noexcept) - Why is
std::vector
fast? - What is the difference between a lambda and using
std::bind
? (think evaluation of arguments) - What are some dangers of using lambdas (in the context of event-driven callback-based design) and how can this be mitigated? (hint:
std::shared_from_this
) - What pattern does
std::shared_from_this
implement? (crtp) - What is the size of a lambda?
- Do lambdas allocate memory on the heap?
- When do you need to declare lambdas mutable?
- Why does
std::thread
take in parameters by value (by default)? - What is the "deducing this" feature added in C++23 and how does it simplify CRTP?
- What is the difference between
std::any
andstd::variant
? - What are you favourite features introduced in C++17?
- What is the difference between
constexpr
andconsteval
? - What is a concept in C++20?
- What is
std::enable_if
and what is its purpose in template metaprogramming? - What is folding in the context of parameter packs?
- What is small buffer optimisation? Give two examples where SBO may be used in the STL.
- Can you deadlock with a single thread? (yes, by acquiring std::mutex twice)
- Can
alignas
affect the size of a struct? - What's the sizeof these structs: struct A { char x; double y; } and struct B { double x; char y; }? (hint: they are the same, think of alignment and the formal definition of
sizeof()
) - What does it mean for a function to be declared inside a
extern "C"
scope in C++? - What is argument dependent lookup in C++?
- What are the two stages in template compilation?
- What is the difference between
push_back()
andemplace_back()
onstd::vector
?