QNA | C++

The “diamond problem” is an ambiguity that can occur in languages that support multiple inheritance, such as C++. It’s called the “diamond problem” because of the shape of the class diagram, which resembles a diamond. It means that we cannot create hybrid inheritance using multiple and hierarchical inheritance.

For example:

class Base {
public:
    void foo();
};

class Derived1 : public Base {
    // ...
};

class Derived2 : public Base {
    // ...
};

class Diamond : public Derived1, public Derived2 {
    // ...
};

In this example, if foo() is called, on an object of type Diamond, it’s ambiguous whether it should call Derived1::foo() or Derived2::foo(), because both Derived1 and Derived2 are derived from Base and could have overridden foo().

To resolve this ambiguity, C++ provides the virtual keyword to specify that only one shared instance of the base class should be inherited:

class Base {
public:
    void foo();
};

class Derived1 : virtual public Base {
    // ...
};

class Derived2 : virtual public Base {
    // ...
};

class Diamond : public Derived1, public Derived2 {
    // ...
};

Now, Diamond will only have one Base, and calling foo() is unambiguous. This is known as “virtual inheritance”.

What is the difference between class and struct?

The only difference between a class and struct are the access modifiers. Struct members are public by default; class members are private. It is good practice to use classes when you need an object that has methods and structs when you have a simple data object.

Lvalue and Rvalue

In C++, lvalue and rvalue are expressions that represent “left value” and “right value” respectively. They are used to describe two categories of expressions that can appear on the left-hand side or the right-hand side of an assignment operator.

  • lvalue (locator value): An lvalue is an expression that refers to a memory location and allows us to take the address of that memory location via the & operator. It can appear either on the left-hand or the right-hand side of an assignment operator. Examples of lvalue: variables, dereference of a pointer, or return value of a function returning by reference.
  • rvalue (read value): An rvalue is an expression that cannot have a value assigned to it which means an rvalue may appear on the right- but not left-hand side of an assignment operator. Rvalues are typically temporary objects or values not associated with a memory location. Examples of rvalue: literals (like 10, 20.5), a temporary object or the result of an expression that generates a temporary.

In C++11 and later, these concepts are further refined with the introduction of lvalue references and rvalue references, which are used to implement features like move semantics and perfect forwarding.

Volatile keyword

The volatile keyword tells the compiler that a variable’s value can change unexpectedly, so it shouldn’t optimize out reads or writes to that variable. Variables that are declared as volatile will not be cached by the compiler, and will thus always be read from memory. However, volatile does not guarantee atomicity (an operation completes in a single step without interruption) or visibility (changes made by one thread are immediately seen by others). Therefore, it’s not suitable for synchronization in multithreaded programs. Instead, use C++ Standard Library facilities like std::atomic, std::mutex, and std::lock_guard for safe multithreaded access

Mutable keyword

The mutable keyword in C++ is used to allow a particular data member of an object to be modified even if the object is declared as const. Normally, when an object is const, it means that none of its data can be changed. However, there may be cases where you want to allow certain data members of a const object to be changed.

For example:

class MyClass {
public:
    mutable int counter;
    int value;

    void increment() const {
        counter++;  // This is allowed because counter is declared as mutable
        // value++; // This would be an error because value is not mutable
    }
};

In this example, increment() is a const member function, which means it’s not allowed to modify the object. However, because counter is declared as mutable, it can be modified inside increment().

Explain the different ways of type casting

  • dynamic_cast: It can only within the context of a class hierarchy for pointers and references to classes (or with void*). It can upcast (converting from pointer-to-derived to pointer-to-base) and can also downcast(convert from pointer-to-base to pointer-to-derived) polymorphic classes (those with virtual members) if-and-only-if the pointed object is a valid complete object of the target type.
  • static_cast: can perform upcasts and downcasts between pointers of related classes. No checks are performed during run-time to guarantee that the object being casted is in fact a full object of the destination type.  No checks are performed during run time to guarantee that the object being casted is in fact a full object of the destination type. It does not incur the overhead of the type-safety checks of dynamic_cast.
  • reinterpret_cast: converts any pointer type to any other pointer type, even of unrelated classes. The operation result is a simple binary copy of the value from one pointer to the other. All pointer conversions are allowed: neither the content pointed nor the pointer type itself is checked.
  • const_cast: manipulates the constness of the object pointed by a pointer, either to be set or to be removed. For example, in order to pass a const pointer to a function that expects a non-const argument:

Q) What is a friend class and when it is used?

A friend class is one that is allowed access to the private and protected members of any class that has declared it a friend. The property is not inherited (sub classes of friends do not become friends automatically), and not transitive (friends of friends are not friends).

Q) What are the advantages and disadvantages of using recursion? Where it is well suited?

Recursion is often slower compared to its iterative counterpart mainly because, a recursive function uses more memory. Each recursive function call reserves additional space on the stack, causing it to grow.
If the recursion is too “deep”,it can result in a “stack overflow” error.

ExamplesTree structures, such as a binary search tree,  when there is a variable number (or a very large number) of nested loops,  Sorting algorithms such as Merge sort, Quick sort, and Bubble sort are all easier to implement recursively

Q) Describe unique_ptr, shared_ptr, weak_ptr

  • A unique pointer (unique_ptr) is a smart pointer that will automatically deallocate the reserved memory as soon as it is out of scope. Only one unique_ptr can point to a resource, so it is illegal (at compile time) to make a copy of a unique_ptr.
  • A shared pointer (shared_ptr) is also a smart pointer that automatically deallocates the reserved memory once it is out of scope. However, a single resource can be simultaneously referenced by many shared pointers. An internal reference counter is used to keep track of the shared_ptr count for any given resource. If there are no references then the memory is freed. This is susceptible to the problem of circular dependency in cases where two or more shared pointers reference each other.
  • A weak pointer (weak_ptr) indeed references the memory, but is not able to use it before being converted into a shared pointer. This is done by using the lock() member function. Using weak pointers is a common way to deal with the problem of circular dependencies. There is no reference count, but as such, the resource is first verified as soon as lock() is called. If the memory is still available then it is usable as a shared pointer. However, had it been deallocated previously then the lock() would fail.

Q) What are functors and their advantages?

Functor (also called function object) is a C++ class with operator() defined. This lets the user create objects that can used like a function. These are effectively used in C++ STLs.

Here is how functors are declared:

#include <algorithm>
#include <iostream>

using namespace std;

int add(int x)
{
   return (x + 1);
}

class functorAdd
{
private:
   int x;
public:
   functorAdd(int val) : x(val) {}
   int operator()(int y) const
   {
      return x + y;
   }
};
//------------------------------
int main()
{
   int intArray[] = { 1, 2, 3, 4, 5 };
   int n = sizeof(intArray) / sizeof(intArray[0]);

   transform(intArray, intArray + n, intArray, functorAdd(10));

   for (int i = 0; i < n; i++)
      cout << intArray[i] << " ";
   return 0;
}
  • It can be used as functions and can be passed as function where a normal function is expected.
  • Functor are highly customizable. e.g. In the above program, add() function could have been used but that would have required, hard-coding of value 10.

In the above program, we are trying to add 10 to each item of array. The output of above program will be “11 12 13 14 15”.

Q) What is lambda and how do they compare with functors and functions?

A lambda is a syntactic shortcut for a functor, and can be used replace functors.

  • captures: The capture clause specifies which outside variables are available for the lambda function and whether they should be captured by value (copying) or by reference.
  • parameters: This specifies set of parameter that can be passed to a lambda function. It can be omitted if the function takes zero arguments.
  • return-type: It declares the return type of lambda function. 
  • statements: This is the lambda body. The statements within the lambda body can access the captured variables and the parameters.
#include 
using namespace std;

int main()
{
  auto lf = []() { cout << "statement" << endl; };
  lf(); //invoke lambda
}

In the above example the lambda function can be written in the following ways that are equivalent

auto lambda = [] { cout << "statement" << endl; };
auto lambda = [](void) { cout << "statement" << endl; };
auto lambda = [](void) -> void { cout << "statement" << endl; };

Notes:

  • A lambda is a syntactic shortcut for a functor, and can be used replace functors.
  • Functors and lambdas always received this pointer, but plain functions dont. This requires few extra bytes in the stack space
  • Lambda “constructors” are inlined in the function they are created

 Here is the example of function, functor and lambda doing the same thing:

#include <algorithm>
#include <iostream>

using namespace std;

int function(int param)
{
  return 10 * param;
}

class Functor
{
public:
int operator()(int param)
{
  return 10 * param;
}
};

int main()
{
  auto lambda = [](int param)
  { return 10 * param; };

  Functor functor;

  int function_out = function(3);
  int functor_out = functor(3); 
  int lambda_out = lambda(3);
  cout << "function_out:" << function_out << endl;
  cout << "functor_out:" << functor_out << endl;
  cout << "lambda_out:" << lambda_out << endl;
  return 0;
}

The output of above program is:
function_out:30
functor_out:30
lambda_out:30

Q) Difference between auto_ptr and unique_ptr

  • A auto_ptr can be copied but unique_ptr can only be moved.
  • unique_ptr can handle arrays correctly and will call delete[], while auto_ptr will attempt to call delete
  • unique_ptr can be stored in containers but auto_ptr cannot.

Q) How can a C function be called in a C++ program?

Using an extern “C” declaration:

//C code
void func(int i){/* code */}
void print(int i){/* code */}
//C++ code
extern "C"{
void func(int i);
void print(int i);
}

void myfunc(int i)
{
func(i);
print(i);
}