C++ Module 7
Classes, objects and RAII interview questions
Interview questions on classes, objects and RAII: short warm-ups, then what each harder question tests, the first step of a good answer and its follow-ups.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher
What does const after a member function's parameter list mean?
Answered in the lesson C++ classes, member functions and access control
-
Fresher
Why should most constructors that take one argument be explicit?
Answered in the lesson Constructors, member initialiser lists and explicit
-
Fresher
What does std::vector<int> v{3, 7}; contain, and how is that different from v(3, 7)?
Answered in the lesson Brace initialisation and initializer_list traps
-
Fresher
What does RAII mean, and what does it guarantee?
-
Fresher
What are the rule of zero, the rule of three and the rule of five?
Answered in the lesson The rule of zero, three and five in C++
-
Fresher
Why is operator+= usually a member function while operator+ is a non-member?
Answered in the lesson C++ operator overloading and the spaceship operator
-
Fresher
What is the difference between writing operator<< for a type and specialising std::formatter for it?
Answered in the lesson Printable types: operator<< and std::formatter
-
Fresher
How does a static member function differ from an ordinary member function?
Answered in the lesson C++ static members, friends and nested types
-
Fresher
What does this auto&& self deduce for a non-const lvalue, a const lvalue and an rvalue object?
Answered in the lesson Deducing this: explicit object parameters in C++23
-
Fresher
What is a strong type, and why is using Paise = std::int64_t; not one?
Answered in the lesson Designing value types and strong types in C++
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Fresher to mid
An Account class exposes setBalance(long long) and getBalance(), and a production bug left one balance negative. How would you redesign the class so that this cannot happen again, and how would you move hundreds of existing callers to the new interface safely?
- What it tests
- Designing an interface around an invariant, preferring operations to setters, and migrating many callers in steps without breaking the build.
- First step
- State the invariant first: the balance is never negative and changes only through deposits, withdrawals and transfers that check their arguments before changing anything. Then list every caller of setBalance and group the calls by what they mean, because each group becomes one named operation.
- Follow-ups
-
- Some callers set the balance when they load an account from the database. What do you give them?
- How do you stop new code from calling setBalance while the migration runs?
- Should a broken invariant be an assertion or an error?
From the lesson C++ classes, member functions and access control
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A function takes const Order& and fails to compile because it calls order.total(), which is not const. A teammate proposes const_cast to make it build. What do you do instead, and when is const_cast actually safe?
- What it tests
- Reading the const error correctly, const member functions, mutable caches and their thread safety, and why writing to a const object through const_cast is undefined behaviour.
- First step
- Read what total() does. If it only reads, the error is a missing promise: mark total() const, which is the whole fix. If it writes a cached result, the cache does not change what the order represents, so that one member may be declared mutable.
- Follow-ups
-
- total() caches its result in a member. What changes for threads?
- When does const_cast not cause undefined behaviour?
- How do you make a large code base const-correct without one huge change?
From the lesson C++ classes, member functions and access control
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid
Sensor derives from Labelled and initialises it with one of its own members: Sensor(const std::string& id) : id_{"sensor-" + id}, Labelled{id_} {}. Every label comes out empty, and AddressSanitizer reports nothing. Explain why, and give two fixes.
- What it tests
- The order in which bases and members are initialised, why reading a member before its construction is undefined, which warnings catch it, and the base-from-member idiom.
- First step
- Base classes are initialised before any member, whatever the order of the list, so Labelled{id_} copies a std::string that has not been constructed yet. That read is undefined behaviour; an empty label is one possible result, not a guarantee.
- Follow-ups
-
- What do the two compilers report, and with which options?
- How does the base-from-member idiom work?
- When is it enough to pass the constructor parameter instead?
From the lesson Constructors, member initialiser lists and explicit
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid
A logging API has log(bool flag) and log(const Message& m), and Message has a non-explicit constructor from std::string. The call log("disk full") prints "flag: true". Explain why the compiler chose that overload without a warning, and redesign the API so the mistake cannot happen.
- What it tests
- Ranking standard against user-defined conversions, the limit of one user-defined conversion, pointer-to-bool conversion, and API design with exact-match overloads, strong types and explicit.
- First step
- A string literal decays to const char*, and a pointer converts to bool by a standard conversion. Reaching Message needs two user-defined conversions, const char* to std::string and std::string to Message, and an implicit conversion may use only one, so log(const Message&) is not even viable.
- Follow-ups
-
- Would giving Message a constructor from const char* fix it?
- Which overloads would you add or delete?
- Where else does the pointer-to-bool conversion cause bugs?
From the lesson Constructors, member initialiser lists and explicit
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 2 minutes
-
Mid
A helper std::initializer_list<int> default_limits() { return {1, 2, 3}; } passes every unit test, but a release build sums the limits to 0 instead of 6. Explain why, say what the compilers and tools report, and give two safer designs.
- What it tests
- The lifetime of the array behind an initializer_list, why undefined behaviour passes debug tests, default warnings and sanitizer options, and owning alternatives.
- First step
- The braces create a temporary array that lives only until the return statement finishes, and the returned initializer_list is just a pointer and a length into that dead array. Reading it is undefined behaviour, so a debug build may still find the old values while an optimised build does not.
- Follow-ups
-
- Which warnings and sanitizer options catch it?
- Why does the same code work when the list is a function parameter?
- Which return type would you choose, and what does it cost?
From the lesson Brace initialisation and initializer_list traps
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior
You maintain a widely used Matrix class with a Matrix(int rows, int cols) constructor and want to add a constructor that takes a brace list of values. What can break for existing users, how do you find it, and how would you design the API to avoid the problem?
- What it tests
- API evolution with list initialisation, how a new initializer_list constructor silently changes existing brace calls, and safer designs.
- First step
- Every existing Matrix m{3, 4}; would switch from the rows and columns constructor to the new list constructor without any compile error, because two ints fit a list of doubles. Calls with parentheses keep their meaning, so the brace calls are the ones to find.
- Follow-ups
-
- How do you find every affected call in a large code base?
- What alternative designs avoid the ambiguity?
- Would making the new constructor explicit help?
From the lesson Brace initialisation and initializer_list traps
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid
A review finds std::lock_guard<std::mutex>{mtx}; at the top of a function that updates shared data, yet a data race shows up under load. Explain why the lock protects nothing, give the fix, and say how you would stop this bug from coming back.
- What it tests
- Temporary lifetimes, RAII objects that must be named, and catching the mistake with warnings, linters and thread sanitizers.
- First step
- The guard has no name, so it is a temporary, destroyed at the end of its full expression: the mutex is unlocked before the next line runs. Give it a name, std::lock_guard guard{mtx};, so that it lives until the end of the scope.
- Follow-ups
-
- What does std::lock_guard<std::mutex>(mtx); with parentheses do?
- Which tools would catch it automatically?
- Why is std::scoped_lock often preferred in new code?
From the lesson Destructors and RAII in C++
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A service sometimes crashes after main returns. A global Cache in cache.cpp writes a summary through a global Logger from logger.cpp in its destructor, and the crash appeared after a change to the link order. Explain the cause and give two fixes, with their costs.
- What it tests
- Initialisation and destruction order of globals across translation units, why it depends on the build, and the construct-on-first-use and never-destroyed fixes.
- First step
- Globals in different .cpp files are initialised in an order the standard leaves unspecified, in practice often the link order, and destroyed in the reverse order of their construction. If the Logger was built after the Cache, it is destroyed first, and the Cache destructor uses a dead object.
- Follow-ups
-
- When does a function-local static not help?
- What does a logger that is never destroyed cost?
- Which tools can find these problems?
From the lesson Destructors and RAII in C++
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
After a refactor that stores Image objects in a std::vector<Image>, the program crashes with a double free. Image holds a raw unsigned char* from new[] and deletes it in its destructor. Diagnose the crash and give two fixes, with their trade-offs.
- What it tests
- Recognising a rule-of-three violation, how a vector copies its elements, AddressSanitizer, and choosing between the rule of five and the rule of zero.
- First step
- The compiler-generated copy constructor copies the pointer, and the vector copies Image objects on insertion and when it grows, so two Images end up deleting one buffer. AddressSanitizer confirms it: both the first free and the double free happen in ~Image.
- Follow-ups
-
- Why did the bug appear only after the refactor?
- Which fix would you choose for a 4K image buffer, and why?
- How would you make Image move-only if copies are too expensive?
From the lesson The rule of zero, three and five in C++
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A Shape base class declares only virtual ~Shape() = default; and a name string. Growing a std::vector<Circle> copies every element, and Shape s = circle; compiles and loses the circle's data. Explain both problems and write the special member functions the base class should declare.
- What it tests
- How a user-declared destructor in a base removes its moves, why derived moves then copy and may throw, slicing, and protected copy and move operations.
- First step
- The virtual destructor is user-declared, so Shape has no implicit move operations. Circle's implicit move constructor must then copy the Shape part, which can throw, so Circle's move is not noexcept and the vector copies on growth. The slicing comes from Shape's public copy constructor.
- Follow-ups
-
- How many copies does growing a vector of 1,000 Circles make, before and after the fix?
- How do you copy a Shape when you do not know its dynamic type?
- Why make the copy and move operations protected rather than deleted?
From the lesson The rule of zero, three and five in C++
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
During a C++20 migration, the six hand-written comparison operators of a Customer class, which compared names without regard to case and ignored a cached hash member, were replaced by one defaulted operator<=>. Unit tests now fail and a std::set of customers holds near-duplicates. What changed, and how do you migrate safely?
- What it tests
- Knowing exactly what a defaulted <=> compares, choosing the comparison category, and changing operators without changing behaviour.
- First step
- Write down what the old operators compared and what the defaulted one compares: every member in declaration order, the cached hash included, with std::string's case-sensitive <=>. Both the members and the kind of order changed, so the fix is a hand-written <=> returning std::weak_ordering, with a matching ==, checked against the old behaviour.
- Follow-ups
-
- Why does == still compile when only <=> was defaulted?
- Why does the comparison category matter for std::set and std::sort?
- How would you prove that the migration changed nothing?
From the lesson C++ operator overloading and the spaceship operator
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A rules engine overloads && and || on a Condition type so that rules read like has_user(req) && is_admin(req). After a refactor that made both functions return Condition, rules crash on requests without a user, although the same rule on bools was safe. Explain the crash and propose a design that keeps rules readable.
- What it tests
- Short-circuit evaluation, why overloaded logical operators lose it, and designing lazy predicate objects instead of eager results.
- First step
- With Condition operands, the expression calls operator&&(has_user(req), is_admin(req)), and every argument of a function call is evaluated before the call, so is_admin(req) runs even when there is no user. Keep the readable syntax but make the operands lazy: combine predicates that are evaluated later with the built-in &&.
- Follow-ups
-
- Does C++17's rule on the order of evaluation fix the problem?
- How do expression-template libraries keep && lazy?
- What would you add to the code review guidelines?
From the lesson C++ operator overloading and the spaceship operator
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to mid
A Money type prints through a std::formatter whose parse() accepts only {}. A teammate writes std::println("{:>12}", amount) to right-align a column of amounts, and it does not compile, although the same placeholder works for a double. Why, and how do you support fill, alignment and width without reimplementing them?
- What it tests
- What parse() decides, composing a standard formatter instead of rewriting its options, and what a width counts.
- First step
- parse() alone defines which specifications exist. This one returns ctx.begin() while it points at >, so the library reports an unparsed specification while compiling. To get fill, alignment and width, format the amount into a string first and give that string to a std::formatter<std::string_view> member that parsed the specification.
- Follow-ups
-
- Why not derive the Money formatter from std::formatter<double>?
- How do you keep a custom option such as {:in} and still accept a width?
- What does a width of 12 count when the text contains a currency sign such as the rupee sign?
From the lesson Printable types: operator<< and std::formatter
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A reporting service lets customers save their own message templates, such as "Total {:in} for {}", which the server fills in with the C++ formatting library. Code review blocks the change. What can go wrong with format strings that users supply, and how would you build the feature safely?
- What it tests
- Run-time format strings and std::vformat, exceptions, resource limits such as huge widths, and choosing a template language that exposes only what customers need.
- First step
- A stored template is known only at run time, so the compile-time check cannot help: it must go through std::vformat, which throws std::format_error for a bad template and obeys any width, even {:1000000000}. Treat the template as untrusted input: validate it when it is saved, limit what it may ask for, and catch errors when it is used.
- Follow-ups
-
- What happens with a template that uses {3} when there are only two arguments?
- Why is this safer than passing a user's string to printf, and what risk remains?
- How would you offer named fields such as {total} instead?
From the lesson Printable types: operator<< and std::formatter
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
An IdGenerator hands out order ids with a static std::uint32_t counter and return ++counter_;. Under load, two orders occasionally get the same id. Explain why, fix it, say what your fix costs, and name what else could still give two orders the same id.
- What it tests
- Data races on static members, atomic read-modify-write operations and memory order, and the limits of an in-process counter: overflow, restarts and several processes.
- First step
- ++counter_ reads, adds and writes back in separate steps, so two threads can read the same value and return the same id; formally it is a data race, which is undefined behaviour. Make the counter a std::atomic and take ids with fetch_add, which performs the whole step as one indivisible operation.
- Follow-ups
-
- Which memory order does fetch_add need here?
- How long until a 32-bit counter wraps around?
- What changes when the service runs as several processes?
From the lesson C++ static members, friends and nested types
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
Box::value() is written once with an explicit object parameter and returns std::forward_like<Self>(self.value_). Code such as const std::string& name = make_box().value(); now crashes, while std::string name = make_box().value(); works. Explain both lines, and how you would design value() for rvalue objects.
- What it tests
- Lifetimes of temporaries, when binding a reference extends a lifetime, what forward_like returns for an rvalue object, and returning by value from rvalue-qualified accessors.
- First step
- make_box() returns a temporary Box that dies at the end of the full expression. For that rvalue, value() returns a std::string&& into the temporary, and binding a reference to a function's result does not extend the temporary's life, so name dangles. The second line moves the string into a new object before the Box is destroyed.
- Follow-ups
-
- Why does lifetime extension not apply to the first line?
- Which signature avoids the problem, and what does it cost?
- Would the old four-overload version have had the same bug?
From the lesson Deducing this: explicit object parameters in C++23
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Senior to staff
A team proposes to replace both a CRTP mixin and a virtual plugin interface with C++23 explicit object parameters, to modernise the code base. Which of the two can it replace and which not, and what would you check before you approve the change?
- What it tests
- Static against dynamic polymorphism, deduction from the static type at each call, and the effect on headers, ABI, build time and call sites.
- First step
- Separate the two uses. The CRTP mixin only needs the derived type while compiling, which an explicit object parameter deduces directly. The plugin interface chooses a function at run time from the object's real type, which deduction cannot do, because self is deduced from the type the compiler sees at the call.
- Follow-ups
-
- What happens when code calls the new base function through a base-class reference?
- Does the change affect class layout or the ABI of a shared library?
- Where is CRTP still the better tool?
From the lesson Deducing this: explicit object parameters in C++23
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A Date class throws std::invalid_argument from its constructor for impossible dates. A CSV importer that reads millions of rows, about one in five with a bad date, is now slow and full of try blocks. How would you redesign the API, and what would you keep?
- What it tests
- The cost of throwing, expected failures against exceptional ones, factories returning std::expected, and keeping one validation rule behind every way of creating a Date.
- First step
- Separate two kinds of caller. A bad date in a file is ordinary input, so the importer needs a factory that returns std::expected<Date, DateError> and never throws. Code that builds dates from values it controls can keep a throwing constructor, as long as both paths share one validation function.
- Follow-ups
-
- Why is throwing so much more expensive than returning an error value?
- Should the throwing constructor stay public?
- How does the importer report which row and column failed, and why?
From the lesson Designing value types and strong types in C++
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes