Interview

15 Modern C++ Interview Questions and Answers

Prepare for your C++ interview with this guide on Modern C++ concepts, featuring curated questions and answers to enhance your understanding and skills.

Modern C++ has evolved significantly with the introduction of new standards like C++11, C++14, C++17, and C++20, bringing powerful features and enhancements to the language. These updates have made C++ more efficient, expressive, and easier to use, while maintaining its core strengths of performance and control over system resources. As a result, C++ remains a critical language in areas such as systems programming, game development, and high-performance applications.

This article aims to prepare you for technical interviews by providing a curated selection of questions and answers focused on Modern C++. By understanding these concepts and practicing the provided examples, you will be better equipped to demonstrate your proficiency and problem-solving abilities in C++ during your interview.

Modern C++ Interview Questions and Answers

1. Explain the differences between std::unique_ptr, std::shared_ptr, and std::weak_ptr.

In Modern C++, smart pointers manage dynamic memory safely and efficiently. The primary types are std::unique_ptr, std::shared_ptr, and std::weak_ptr.

std::unique_ptr maintains exclusive ownership of a dynamically allocated object. It cannot be copied, only moved, ensuring a single owner and automatic deletion when the owner is done with it.

std::shared_ptr allows shared ownership of a dynamically allocated object. The object is destroyed when the last std::shared_ptr owning it is destroyed or reset, useful for shared access to the same resource.

std::weak_ptr provides a non-owning reference to an object managed by std::shared_ptr, useful for breaking circular references that can lead to memory leaks.

Example:

#include <iostream>
#include <memory>

void uniquePtrExample() {
    std::unique_ptr<int> uniquePtr = std::make_unique<int>(10);
    std::cout << "UniquePtr value: " << *uniquePtr << std::endl;
}

void sharedPtrExample() {
    std::shared_ptr<int> sharedPtr1 = std::make_shared<int>(20);
    std::shared_ptr<int> sharedPtr2 = sharedPtr1; // shared ownership
    std::cout << "SharedPtr value: " << *sharedPtr1 << std::endl;
    std::cout << "SharedPtr use count: " << sharedPtr1.use_count() << std::endl;
}

void weakPtrExample() {
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(30);
    std::weak_ptr<int> weakPtr = sharedPtr; // non-owning reference
    std::cout << "WeakPtr use count: " << weakPtr.use_count() << std::endl;
    if (auto lockedPtr = weakPtr.lock()) {
        std::cout << "WeakPtr value: " << *lockedPtr << std::endl;
    }
}

int main() {
    uniquePtrExample();
    sharedPtrExample();
    weakPtrExample();
    return 0;
}

2. Implement a class with a move constructor and a move assignment operator.

Move semantics optimize resource management by transferring resources from one object to another, rather than copying them. This is beneficial for classes managing dynamic memory or other resources, improving performance by avoiding unnecessary deep copies.

To implement a class with a move constructor and a move assignment operator, define special member functions that handle resource transfer. Here is an example:

#include <iostream>
#include <utility>

class Resource {
public:
    int* data;

    // Constructor
    Resource(int value) : data(new int(value)) {}

    // Destructor
    ~Resource() {
        delete data;
    }

    // Move Constructor
    Resource(Resource&& other) noexcept : data(other.data) {
        other.data = nullptr;
    }

    // Move Assignment Operator
    Resource& operator=(Resource&& other) noexcept {
        if (this != &other) {
            delete data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }

    // Disable Copy Constructor and Copy Assignment Operator
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;
};

int main() {
    Resource res1(10);
    Resource res2 = std::move(res1); // Move constructor
    Resource res3(20);
    res3 = std::move(res2); // Move assignment operator

    return 0;
}

3. Write a constexpr function that calculates the factorial of a number.

constexpr allows the evaluation of functions and expressions at compile time, reducing runtime computations. A constexpr function can be evaluated at compile time if its arguments are known at compile time.

Here is an example of a constexpr function to calculate the factorial of a number:

constexpr int factorial(int n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));
}

int main() {
    constexpr int result = factorial(5); // result is 120
    return 0;
}

4. Use std::async to run a function asynchronously and retrieve its result using futures.

std::async runs a function asynchronously and returns a std::future object to retrieve the result once complete. This is useful for parallelizing tasks and improving application performance by utilizing multiple threads.

Example:

#include <iostream>
#include <future>
#include <thread>

int compute_sum(int a, int b) {
    std::this_thread::sleep_for(std::chrono::seconds(2)); // Simulate a long computation
    return a + b;
}

int main() {
    std::future<int> result = std::async(std::launch::async, compute_sum, 5, 10);

    // Do other work while the computation is happening

    std::cout << "The sum is: " << result.get() << std::endl; // Retrieve the result
    return 0;
}

5. Demonstrate the use of SFINAE to enable a function only if a certain condition is met.

SFINAE (Substitution Failure Is Not An Error) allows the compiler to ignore certain template instantiations that do not meet specific criteria. This is useful for enabling or disabling functions based on type traits or other conditions.

Example:

#include <iostream>
#include <type_traits>

// Function enabled only if T is an integral type
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
printIfIntegral(T value) {
    std::cout << "Integral value: " << value << std::endl;
}

// Function enabled only if T is a floating-point type
template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
printIfFloatingPoint(T value) {
    std::cout << "Floating-point value: " << value << std::endl;
}

int main() {
    printIfIntegral(42);          // Works, as 42 is an integral type
    printIfFloatingPoint(3.14);   // Works, as 3.14 is a floating-point type
    return 0;
}

6. Use std::optional to handle a function that may or may not return a value.

std::optional represents optional values, useful for functions that may or may not return a meaningful result. It avoids the use of pointers or special sentinel values to indicate the absence of a value.

Example:

#include <iostream>
#include <optional>
#include <string>

std::optional<std::string> findNameById(int id) {
    if (id == 1) {
        return "Alice";
    } else if (id == 2) {
        return "Bob";
    } else {
        return std::nullopt; // No value
    }
}

int main() {
    auto name = findNameById(1);
    if (name) {
        std::cout << "Name found: " << *name << std::endl;
    } else {
        std::cout << "Name not found" << std::endl;
    }

    name = findNameById(3);
    if (name) {
        std::cout << "Name found: " << *name << std::endl;
    } else {
        std::cout << "Name not found" << std::endl;
    }

    return 0;
}

7. Implement a function that uses std::variant to handle multiple types.

std::variant is a type-safe union that can hold one of several specified types, ensuring only one type is active at any given time.

Example:

#include <iostream>
#include <variant>
#include <string>

using VarType = std::variant<int, float, std::string>;

void processVariant(const VarType& var) {
    std::visit([](auto&& arg) {
        using T = std::decay_t<decltype(arg)>;
        if constexpr (std::is_same_v<T, int>) {
            std::cout << "Integer: " << arg << std::endl;
        } else if constexpr (std::is_same_v<T, float>) {
            std::cout << "Float: " << arg << std::endl;
        } else if constexpr (std::is_same_v<T, std::string>) {
            std::cout << "String: " << arg << std::endl;
        }
    }, var);
}

int main() {
    VarType v1 = 42;
    VarType v2 = 3.14f;
    VarType v3 = std::string("Hello, World!");

    processVariant(v1);
    processVariant(v2);
    processVariant(v3);

    return 0;
}

8. Use structured bindings to unpack a tuple returned by a function.

Structured bindings allow you to unpack tuples, pairs, and other types directly into individual variables, providing a more readable way to work with these types.

Example:

#include <tuple>
#include <iostream>

std::tuple<int, double, std::string> getTuple() {
    return std::make_tuple(1, 2.5, "example");
}

int main() {
    auto [a, b, c] = getTuple();
    std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
    return 0;
}

9. Explain how concepts can be used to constrain template parameters.

Concepts specify constraints on template parameters, ensuring that the types used in templates meet certain criteria. This makes the code more readable and easier to debug.

Example:

#include <concepts>
#include <iostream>

template<typename T>
concept Integral = std::is_integral_v<T>;

template<Integral T>
T add(T a, T b) {
    return a + b;
}

int main() {
    std::cout << add(3, 4) << std::endl; // Works fine
    return 0;
}

10. Use the ranges library to filter and transform a sequence of elements.

The ranges library offers a powerful way to work with sequences of elements, allowing for operations such as filtering and transforming to be performed in a more readable manner.

Example:

#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    auto even_numbers = numbers 
                        | std::views::filter([](int n) { return n % 2 == 0; })
                        | std::views::transform([](int n) { return n * n; });

    for (int n : even_numbers) {
        std::cout << n << " ";
    }

    return 0;
}

11. Implement the three-way comparison operator for a custom class.

The three-way comparison operator (<=>) allows for a unified approach to comparison operations, returning a value that can be used to determine the ordering of two objects.

Example:

#include <compare>
#include <iostream>

class MyClass {
public:
    int value;

    MyClass(int v) : value(v) {}

    auto operator<=>(const MyClass& other) const = default;
};

int main() {
    MyClass a(10);
    MyClass b(20);

    if (a < b) {
        std::cout << "a is less than b" << std::endl;
    } else if (a > b) {
        std::cout << "a is greater than b" << std::endl;
    } else {
        std::cout << "a is equal to b" << std::endl;
    }

    return 0;
}

12. Explain the C++ memory model and demonstrate the use of atomic operations.

The C++ memory model defines how operations on memory are executed in a concurrent environment, specifying rules for memory ordering, visibility, and synchronization. Atomic operations allow for lock-free, thread-safe manipulation of shared data.

Example:

#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 1000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Counter: " << counter.load() << std::endl;
    return 0;
}

13. Understanding RAII (Resource Acquisition Is Initialization) and its importance.

RAII (Resource Acquisition Is Initialization) ties resource allocation and deallocation to the lifetime of objects, ensuring resources are properly managed and preventing leaks.

Example:

#include <iostream>
#include <fstream>

class FileManager {
public:
    FileManager(const std::string& filename) {
        file.open(filename);
        if (!file.is_open()) {
            throw std::runtime_error("Failed to open file");
        }
    }

    ~FileManager() {
        if (file.is_open()) {
            file.close();
        }
    }

    void write(const std::string& data) {
        if (file.is_open()) {
            file << data;
        }
    }

private:
    std::ofstream file;
};

int main() {
    try {
        FileManager fileManager("example.txt");
        fileManager.write("Hello, RAII!");
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    return 0;
}

14. Explain the use and benefits of constexpr if.

constexpr if allows for compile-time conditional statements, enabling the compiler to evaluate conditions during compilation.

Example:

#include <iostream>
#include <type_traits>

template <typename T>
void printTypeInfo(const T& value) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << "The value is an integral type: " << value << std::endl;
    } else if constexpr (std::is_floating_point_v<T>) {
        std::cout << "The value is a floating-point type: " << value << std::endl;
    } else {
        std::cout << "The value is of some other type: " << value << std::endl;
    }
}

int main() {
    printTypeInfo(42);          // Integral type
    printTypeInfo(3.14);        // Floating-point type
    printTypeInfo("Hello");     // Other type
    return 0;
}

15. Discuss the differences between lvalue and rvalue references and their significance.

Lvalue and rvalue references distinguish between different types of values and optimize resource management. Lvalues refer to objects with identifiable memory locations, while rvalues refer to temporary objects. Move semantics and perfect forwarding optimize performance by transferring resources and efficiently passing arguments.

Example:

#include <iostream>
#include <utility>

class Resource {
public:
    Resource() { std::cout << "Resource acquired\n"; }
    ~Resource() { std::cout << "Resource destroyed\n"; }
    Resource(const Resource&) { std::cout << "Resource copied\n"; }
    Resource(Resource&&) { std::cout << "Resource moved\n"; }
};

void processResource(Resource&& res) {
    std::cout << "Processing resource\n";
}

int main() {
    Resource res1; // lvalue
    processResource(std::move(res1)); // rvalue
    return 0;
}

In this example, the Resource class demonstrates the use of copy and move constructors. The processResource function takes an rvalue reference, allowing it to accept temporary objects efficiently. The std::move function converts an lvalue to an rvalue, enabling the move semantics.

Previous

10 Data Analytics Internship Interview Questions and Answers

Back to Interview
Next

20 Node.js Interview Questions and Answers