← Citadel Interview Insights

Citadel·Software Engineer·Technical Phone Screen·Senior

SeniorPrefer not to say
Jun 2026

Summary

Citadel software engineer interview, technical round focused almost entirely on C++ memory management internals. They wanted a full working SharedPtr implementation from scratch, not just pseudocode, which I wasn't quite expecting at that depth.

Questions Asked (5)

Q1

Implement a templated SharedPtr<T> class in C++ with full reference-counting semantics, including default, raw pointer, copy, and move constructors, copy and move assignment, destructor, and the standard dereference and access operators.

System DesignTechnical Trade-offs
Author's notes

I started with the control block struct and the constructor from a raw pointer, which felt like the right entry point.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by clarifying the ownership semantics and thread-safety requirements, then outline the class design with a control block for the reference count and pointer. Implement each special member function with strong exception safety and move semantics, and finally discuss trade-offs like atomic vs non-atomic reference counting and intrusive vs non-intrusive design.

Pro tip: Mention that you would use a separate control block to store the reference count and deleter, enabling support for custom deleters and weak pointers, and highlight that the reference count should be atomic for thread safety unless explicitly stated otherwise.

1. Clarify Requirements and Constraints

Ask about thread-safety, custom deleter support, and whether weak references are needed. This shows you think about the broader design before coding.

2. Design the Class Structure

Decide on a control block to hold the reference count and pointer, and define the member variables. Consider using a separate control block for flexibility.

3. Implement Constructors and Destructor

Write the default, raw pointer, copy, and move constructors, ensuring proper reference count increments and decrements. The destructor should decrement and delete when count reaches zero.

4. Implement Assignment Operators

Implement copy and move assignment with self-assignment check and strong exception safety, using the copy-and-swap idiom or manual reference count management.

5. Implement Access Operators and Discuss Trade-offs

Provide operator* and operator->, and discuss trade-offs such as atomic vs non-atomic reference counting, intrusive vs non-intrusive design, and performance implications.

Key Points to Mention

  • Reference counting mechanism with atomic operations for thread safety
  • Control block design to support custom deleters and weak pointers
  • Move semantics to avoid unnecessary reference count increments
  • Exception safety in constructors and assignment operators
  • Self-assignment handling in copy assignment
  • Comparison with std::shared_ptr and potential optimizations

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q2

How would you make the reference count in your SharedPtr implementation thread-safe?

Technical Trade-offsSystem Design
Author's notes

Said std::atomic for the count field in the control block, which is the right answer.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by explaining that the reference count must be updated atomically to avoid data races, then discuss using std::atomic<int> with fetch_add/fetch_sub and appropriate memory ordering. Also mention the need to handle the control block safely, including deletion when the count reaches zero, and compare with std::shared_ptr's approach.

Pro tip: Demonstrate awareness of performance trade-offs: atomic operations have overhead, so consider whether relaxed ordering suffices for increments and acquire-release for decrements. Also mention that thread-safety of the reference count doesn't make the pointed-to object thread-safe.

1. Identify the race condition

Explain that concurrent increments/decrements of a plain integer cause data races and undefined behavior, so the reference count must be made atomic.

2. Choose atomic operations

Use std::atomic<int> (or std::atomic<size_t>) and replace ++/-- with fetch_add(1) and fetch_sub(1) to ensure atomicity.

3. Select memory ordering

Use memory_order_relaxed for increments (no synchronization needed) and memory_order_acq_rel for decrements to ensure proper synchronization when destroying the object.

4. Handle deletion safely

When fetch_sub returns 1, the current thread is responsible for deleting the managed object and control block; ensure no other thread can access them afterward.

5. Discuss alternatives and trade-offs

Mention that a mutex or spinlock could work but is heavier; atomic is preferred. Also note that the control block itself must be allocated and managed safely.

Key Points to Mention

  • Data race on non-atomic reference count leads to undefined behavior.
  • Use std::atomic<int> with fetch_add and fetch_sub for atomic updates.
  • Memory ordering: relaxed for increments, acq_rel for decrements to synchronize destruction.
  • The thread that observes the count drop to zero is responsible for deletion.
  • Thread-safety of the reference count does not make the pointed-to object thread-safe.
  • Compare with std::shared_ptr's implementation, which uses atomic reference counting.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q3

What are the trade-offs between a separate control block and an intrusive reference count, and how does that relate to supporting weak_ptr?

Technical Trade-offsSystem Design
Author's notes

This is where the conversation got interesting.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by defining both mechanisms and contrasting their memory layout, performance, and ownership semantics. Then explain how each affects the implementation of weak_ptr, focusing on the need for a separate weak count and the ability to keep the control block alive. Conclude with practical trade-offs and when to choose one over the other.

Pro tip: Mention that intrusive reference counting ties the count to the object, which can be problematic for weak_ptr because the object may be destroyed while weak references remain; this is why separate control blocks are preferred in standard libraries like std::shared_ptr.

1. Define the two approaches

Briefly explain separate control block (external to object) and intrusive reference count (embedded in object). Highlight that intrusive requires object cooperation and cannot be used with arbitrary types.

2. Compare trade-offs

Discuss memory overhead, cache locality, allocation cost, and flexibility. Separate control block adds an extra allocation but allows weak_ptr and works with any type; intrusive avoids extra allocation but requires modifying the object and complicates weak references.

3. Explain weak_ptr support

Describe how weak_ptr needs to know when the object is destroyed without keeping it alive. With a separate control block, the block can outlive the object and hold a weak count; with intrusive counting, the object's destruction would destroy the count, making weak_ptr impossible unless a separate structure is added.

4. Discuss implementation details

Mention that in a separate control block, there are two counts: strong and weak. The object is destroyed when strong count hits zero, but the control block is destroyed when both counts hit zero. This allows weak_ptr to safely check if the object still exists.

5. Conclude with practical implications

Summarize that separate control blocks are more flexible and support weak_ptr naturally, while intrusive counts are more efficient but limit weak_ptr support and require intrusive design. Choose based on performance needs and type constraints.

Key Points to Mention

  • Memory layout: separate control block adds an extra allocation; intrusive count embeds in object.
  • Performance: intrusive has better cache locality and no extra allocation; separate control block may incur allocation overhead.
  • Flexibility: separate control block works with any type; intrusive requires modifying the class.
  • Weak_ptr requires a separate count that can outlive the object; intrusive counting cannot provide this without additional structure.
  • In separate control block, strong and weak counts are independent; object destroyed when strong count reaches zero, control block destroyed when both reach zero.
  • Standard library implementations (e.g., std::shared_ptr) use separate control blocks to support weak_ptr.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q4

Explain the aliasing constructor for shared_ptr and when you'd actually use it.

Technical Trade-offsAPI & Integrations
Author's notes

Blanked for a second.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by defining the aliasing constructor as a way to create a shared_ptr that shares ownership with another shared_ptr but points to a different object (often a subobject). Then explain the mechanics: it takes a shared_ptr (or weak_ptr) for ownership and a raw pointer for the stored pointer. Finally, discuss practical use cases like accessing members of an object managed by shared_ptr while ensuring lifetime extension, and highlight trade-offs such as potential dangling pointers if misused.

Pro tip: Emphasize that the aliasing constructor is not for creating a new independent ownership; it's for safely accessing subobjects or related data within an already-owned object. Mention that it's often used in conjunction with enable_shared_from_this to return shared_ptrs to members without changing ownership semantics.

1. Define the aliasing constructor

Explain that it's a shared_ptr constructor that takes another shared_ptr (or weak_ptr) for ownership and a raw pointer for the stored pointer, allowing the new shared_ptr to share ownership while pointing to a different object.

2. Clarify ownership vs. stored pointer

Distinguish between the ownership (managed by the original shared_ptr) and the stored pointer (the raw pointer). The aliasing shared_ptr keeps the original object alive but dereferences to the raw pointer.

3. Provide a concrete example

Give a code example, such as a class with a member that needs to be shared, or accessing an element in a container owned by a shared_ptr, showing how the aliasing constructor extends lifetime.

4. Discuss use cases

Mention scenarios like returning a shared_ptr to a member from a factory function, or when working with libraries that require shared_ptr to subobjects (e.g., Boost.Asio).

5. Highlight trade-offs and pitfalls

Note that the aliasing constructor can lead to dangling pointers if the raw pointer outlives the owned object, and that it's not a replacement for proper design; use it judiciously.

Key Points to Mention

  • The aliasing constructor signature: template< class Y > shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;
  • It shares ownership with r but stores ptr, so get() returns ptr while use_count() reflects shared ownership.
  • Common use case: returning a shared_ptr to a member of an object managed by shared_ptr, ensuring the object stays alive.
  • It can also be used with weak_ptr to create a shared_ptr that shares ownership with a weak_ptr's control block.
  • Pitfall: if the raw pointer points to an object not owned by the shared_ptr, it can lead to undefined behavior or dangling pointers.
  • Performance: no additional control block allocation; it reuses the existing control block.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q5

What are the pitfalls of reference-counted smart pointers with cyclic ownership, and how does weak_ptr address that?

Technical Trade-offsAlgorithms & Data Structures
Author's notes

Classic question, answered it fine.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by defining reference-counted smart pointers and explaining how cyclic ownership leads to memory leaks. Then describe how weak_ptr breaks cycles by providing a non-owning reference that doesn't affect the reference count, and discuss practical implications like using weak_ptr for back-references or caches.

Pro tip: Mention that weak_ptr requires locking to access the underlying object, which introduces thread-safety considerations and potential overhead. Also, note that cycles can be subtle and tools like Valgrind or AddressSanitizer can help detect them.

1. Define reference-counted smart pointers

Briefly explain that reference-counted smart pointers (e.g., std::shared_ptr) manage object lifetime by counting references and deleting when count reaches zero.

2. Explain cyclic ownership pitfall

Describe how two objects holding shared_ptrs to each other create a cycle, preventing reference counts from reaching zero and causing a memory leak.

3. Introduce weak_ptr as a solution

Explain that weak_ptr provides a non-owning reference to an object managed by shared_ptr, without incrementing the reference count, thus breaking cycles.

4. Discuss usage and trade-offs

Mention that weak_ptr must be converted to shared_ptr via lock() to access the object, and that this adds overhead and requires handling expired pointers.

5. Conclude with practical implications

Summarize that weak_ptr is essential for avoiding leaks in cyclic structures, but should be used judiciously to avoid dangling references and performance costs.

Key Points to Mention

  • Reference counting mechanism and how cycles prevent deallocation.
  • Definition of weak_ptr as a non-owning smart pointer.
  • How weak_ptr breaks cycles by not incrementing the strong reference count.
  • The need to use lock() to obtain a shared_ptr from weak_ptr, and handling expired objects.
  • Common use cases: parent-child relationships, observer patterns, caches.
  • Potential overhead and thread-safety considerations when using weak_ptr.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.