The double-free is obvious once you see it but I fumbled explaining the rule of three clearly under pressure.
First, identify the missing copy operations and explain the shallow copy problem leading to double-free and dangling pointers. Then, discuss the Rule of Three and propose correct implementations using deep copy or move semantics. Finally, mention modern alternatives like smart pointers to avoid manual memory management.
Pro tip: Emphasize that the root cause is the lack of resource ownership semantics, and demonstrate awareness of the Rule of Five and copy-and-swap idiom for exception safety.
Point out that the class has a destructor but no copy constructor or copy assignment operator, violating the Rule of Three.
Describe how the compiler-generated copy operations perform shallow copies, copying the raw pointer value instead of the pointed-to data.
Detail the resulting bugs: double-free when both objects are destroyed, and dangling pointers if one object is modified or destroyed while the other still references the memory.
Suggest implementing deep copy semantics (copy constructor and copy assignment) or deleting copy operations and providing move operations (Rule of Five).
Mention using smart pointers (e.g., std::unique_ptr or std::shared_ptr) to automate memory management and avoid manual resource handling.
AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.
Explain that storing polymorphic objects by value in a std::vector<Shape> causes object slicing, where the Circle's derived parts are lost, and virtual dispatch fails. Then present the fix: store pointers (preferably smart pointers like std::unique_ptr<Shape>) or use a container of references (e.g., std::reference_wrapper<Shape>) to preserve polymorphism.
Pro tip: Mention that even if you use pointers, you must ensure the base class has a virtual destructor to avoid undefined behavior when deleting derived objects. Also, consider the performance and ownership implications of your chosen fix.
State that storing derived objects by value in a vector of base objects causes object slicing, where only the base part is copied.
Describe that slicing leads to loss of derived data and behavior, and virtual functions called on sliced objects will not dispatch to the derived implementation.
Recommend storing pointers to the base class instead of values, such as std::vector<std::unique_ptr<Shape>>, to preserve polymorphism.
Mention other options like std::reference_wrapper or a variant, and discuss ownership, lifetime, and performance considerations.
Emphasize the need for a virtual destructor in the base class and using smart pointers for automatic memory management.
AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.
Swap has to be noexcept, and you construct the new value before modifying the old one so if construction throws you haven't touched the original.
Explain the copy-and-swap idiom step by step: first create a copy of the source object, then swap the copy with the current object, and finally let the copy's destructor clean up the old state. Emphasize that if any operation during the copy phase throws, the original object remains unchanged, thus providing the strong exception guarantee. Then discuss the requirements: swap must be noexcept, and the move constructor must be noexcept to avoid potential exceptions during reallocation or swapping.
Pro tip: Mention that the strong exception guarantee is only provided if the copy constructor provides it, and that swap should be a non-member function found via ADL to avoid unnecessary copies. Also, note that the move constructor is required to be noexcept for containers to use move semantics during reallocation, which is crucial for maintaining the strong guarantee in operations like vector::push_back.
State that the strong exception guarantee means that if an operation throws, the program state remains unchanged (no effects). This is also known as the commit-or-rollback semantics.
Explain the typical implementation: create a temporary copy of the source object, swap the temporary with *this, and let the temporary's destructor release the old resources. This ensures that if the copy constructor throws, *this is untouched.
Highlight that all potentially throwing operations (copy construction, resource allocation) happen before any modification to *this. The swap operation is noexcept, so it cannot throw. Thus, either the operation succeeds completely or *this remains unchanged.
Swap must be noexcept because if it could throw, the strong guarantee would be violated. It should also be efficient (constant time) and typically implemented as a non-member function to avoid unnecessary copies.
The move constructor must be noexcept to ensure that containers can use move semantics during reallocation without risking the strong guarantee. If the move constructor can throw, containers may fall back to copying, which could be less efficient and might not provide the strong guarantee.
AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.
Standard answer: the destructor called is determined statically, so only the base destructor runs, derived members don't get cleaned up, and the standard explicitly calls it UB.
Start by explaining the mechanism: when deleting through a base pointer, the compiler uses the static type to determine which destructor to call. If the destructor is non-virtual, only the base destructor runs, leaving the derived part un-destroyed. Then connect this to undefined behavior: the derived object's resources are not released, and the memory deallocation may use the wrong size or alignment, leading to heap corruption or crashes.
Pro tip: Mention that this is a classic example of why virtual destructors are essential in polymorphic base classes, and note that even if it seems to work in a simple test, it's still undefined behavior and can break with different compilers or optimizations.
Describe how delete expression works: it calls the destructor based on the static type of the pointer, then deallocates memory. With a non-virtual destructor, only the base destructor is invoked.
Point out that the derived class destructor is never called, so any resources it owns (memory, file handles, etc.) are leaked, and its specific cleanup logic is skipped.
Explain that the C++ standard explicitly states that deleting a derived object through a base pointer with a non-virtual destructor is undefined behavior. This is because the deallocation function may not know the correct size or alignment of the derived object.
Mention that in practice, this can cause heap corruption, crashes, or silent resource leaks. The behavior may vary across compilers and platforms, making it unpredictable.
Conclude that the fix is to declare the base class destructor as virtual, ensuring the correct derived destructor is called and proper deallocation occurs.
AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.
Delete the copy constructor and copy assignment, keep or default the move versions.
Start by defining the trade-off: move-only types eliminate accidental copies and enforce unique ownership, but restrict flexibility. Then explain scenarios where move-only is beneficial (e.g., resource-owning types, performance-critical code) and how call sites must change to use std::move and avoid copies. Finally, discuss the impact on APIs and error handling.
Pro tip: Mention that move-only types can improve performance and safety, but also consider the ripple effect on existing code and the need for clear documentation. Also, note that move-only doesn't mean immovable—it can still be moved, which is often sufficient.
Explain that move-only types prevent copying, which can avoid expensive deep copies and enforce unique ownership, but at the cost of reduced flexibility.
Discuss scenarios such as managing unique resources (file handles, sockets), large data structures where copying is expensive, or when you want to enforce single ownership semantics.
Explain that call sites must use std::move to transfer ownership, and that functions taking the type by value will require an rvalue. Also, APIs may need to change to accept rvalue references or return by value.
Mention that move-only types can affect exception safety and error propagation, as you cannot copy to roll back. Consider using smart pointers or optional for nullable move-only types.
Provide a brief example, such as a Buffer class that owns a large array, and show how making it move-only changes a function that previously took it by value.
AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.