The Problem with Shallow Copying
A simple smart pointer implementation that lacks reference counting can suffer from a critical flaw known as the double-free error. This occurs when a copy of the pointer is made, causing both the original and the copy to point to the same underlying resource. When either of these pointers goes out of scope, it will attempt to delete the same memory, leading to undefined behavior.
Consider the following basic smart pointer class. The copy constructor is intentionally omitted to demonstrate the problem.
template<typename t="">
class BasicPointer {
public:
explicit BasicPointer(T* resource = nullptr) : resource_(resource) {}
~BasicPointer() {
delete resource_;
}
T& operator*() { return *resource_; }
T* operator->() { return resource_; }
// Copy constructor is intentionally omitted to show the problem
// BasicPointer(const BasicPointer& other) = delete;
private:
T* resource_;
};
int main() {
BasicPointer<int> sourcePtr(new int(42));
// This line would cause a double-free error because 'destinationPtr'
// would also point to the same memory as 'sourcePtr'.
// BasicPointer<int> destinationPtr(sourcePtr);
return 0;
}
</int></int></typename>
std::auto_ptr: Transferring Ownership
The std::auto_ptr was an early attempt to solve this issue. Instead of performing a shallow copy, it transfers ownership of the managed resource from the source pointer to the destination. After the copy, the source pointer becomes null, ensuring that only one pointer is responsible for the resource.
While it solves the double-free problem, std::auto_ptr has significant drawbacks. It is now deprecated and should not be used in modern C++ code. A major issue is that it can silently transfer ownership during copies, which can lead to unexpected behavior, especially when used with Standard Libray containers like std::vector.
#include <memory>
#include <vector>
#include <iostream>
int main() {
std::auto_ptr<int> sourceAutoPtr(new int(100));
std::auto_ptr<int> destinationAutoPtr(sourceAutoPtr); // Ownership is transferred
// 'sourceAutoPtr' is now null
if (!sourceAutoPtr.get()) {
std::cout << "Source pointer is now null." << std::endl;
}
// This is safe; only 'destinationAutoPtr' owns the resource
*destinationAutoPtr = 200;
std::cout << "Value: " << *destinationAutoPtr << std::endl;
// Example of the danger with STL containers
std::vector<:auto_ptr>> sourceVector;
sourceVector.push_back(std::auto_ptr<int>(new int(1)));
sourceVector.push_back(std::auto_ptr<int>(new int(2)));
std::vector<:auto_ptr>> destinationVector;
// This copy operation will transfer ownership from 'sourceVector' to 'destinationVector',
// leaving 'sourceVector' filled with null pointers.
destinationVector = sourceVector;
return 0;
}
</:auto_ptr></int></int></:auto_ptr></int></int></iostream></vector></memory>
std::scoped_ptr: Forbidding Copies
A more straightforward solution is to simply forbid copying the smart pointer all together. This guarantees that a resource is managed by only one pointer instance. The std::scoped_ptr (or a custom implementation) achieves this by deleting the copy constructor and copy assignment operator.
#include <iostream>
template<typename t="">
class ScopedPointer {
public:
explicit ScopedPointer(T* resource = nullptr) : resource_(resource) {}
~ScopedPointer() {
delete resource_;
}
T& operator*() { return *resource_; }
T* operator->() { return resource_; }
// Copying is explicitly forbidden
ScopedPointer(const ScopedPointer&) = delete;
ScopedPointer& operator=(const ScopedPointer&) = delete;
private:
T* resource_;
};
int main() {
ScopedPointer<int> myPtr(new int(500));
// The following lines would cause a compile-time error:
// ScopedPointer<int> anotherPtr(myPtr);
// ScopedPointer<int> yetAnotherPtr = myPtr;
return 0;
}
</int></int></int></typename></iostream>
std::unique_ptr: The Modern Solution
std::unique_ptr is the recommended smart pointer for exclusive ownership semantics in modern C++. Like scoped_ptr, it forbids copying to ensure a single owner. How ever, it introduces the concept of move semantics, allowing the resource to be explicitly and safely transferred between pointers. This makes ownership transfer clear and unambiguous, unlike the implicit transfer of auto_ptr.
Move semantics are enabled through a move constructor and a move assignment operator. To transfer ownership, you must use std::move, making the intent explicit.
#include <memory>
#include <utility> // For std::move
#include <iostream>
// A factory function that returns a unique_ptr
std::unique_ptr<int> createPointer() {
return std::unique_ptr<int>(new int(999));
}
int main() {
std::unique_ptr<int> sourceUniquePtr(new int(888));
// Ownership is transferred using std::move
std::unique_ptr<int> destinationUniquePtr = std::move(sourceUniquePtr);
// 'sourceUniquePtr' is now null
if (!sourceUniquePtr) {
std::cout << "Source unique_ptr is now null." << std::endl;
}
// This is safe; only 'destinationUniquePtr' owns the resource
*destinationUniquePtr = 1000;
std::cout << "Value: " << *destinationUniquePtr << std::endl;
// Example of using a factory function
std::unique_ptr<int> fromFactory = createPointer();
std::cout << "Value from factory: " << *fromFactory << std::endl;
return 0;
}
</int></int></int></int></int></iostream></utility></memory>