The Clone Smart Pointer class is a smart pointer which acts as a reference variable for comparison and copy operations. When sorted in STL containers (std::map, vector, list, and deque), the container gets sorted based on the object pointed to, and NOT by the pointer address.
Introduction
The clone_ptr
class is a smart pointer that is designed to work with STL containers for comparison and copy operations of the object itself, instead of the object's address. It does this by acting as a reference variable when performing these operations. The assignment operation causes a unique duplicate of the object, because clone_ptr
behaves as a reference variable and because it’s a non-sharing smart pointer.
It's able to duplicate itself to the correct derived type without requiring the base class to have a clone method. Moreover the clone pointer has operator methods for assignment, equal, and greater than that calls the object's assignment operator. That means when a sort is performed on a container of clone pointers, the object being pointed to is sorted, and not the pointer address.
Unlike share pointers, the clone_ptr
class does not share its pointer, and it's based on the idea of strict pointer ownership logic. That means the content of one container of clone pointers can be copied to another container without having two containers pointing to the same data.
Background
I decided to create a new smart pointer, because the common available smart pointers are not well suited for STL containers. In accordance with C++ standards, the std::auto_ptr
cannot be used at all with STL containers. The boost shared_ptr
class would not be a good choice if you need to copy the objects from one container to another.
One of the main logic differences between the clone_ptr
class and other smart pointers like boost::shared_ptr
and auto_ptr
, is that the clone_ptr
attempts to emulate a reference variable (concrete type) via pointer interface, whereas boost::shared_ptr
and std::auto_ptr
attempt to emulate a pointer. The disadvantage of emulating a pointer is that it brings in logic that is not usually beneficial, and often detrimental to the desired effect.
Here is an example of undesirable pointer behavior in a container.
vector<std::shared_ptr<std::string> > vStrings;
vStrings.push_back(std::make_shared<string>(std::string("Jill")));
vStrings.push_back(std::make_shared<string>(std::string("Tom")));
vStrings.push_back(std::make_shared<string>(std::string("David")));
vStrings.emplace_back(new std::string("Zulma"));
vStrings.emplace_back(new std::string("Yari"));
vStrings.emplace_back(new std::string("Bill"));
sort(vStrings.begin(), vStrings.end()); for (size_t i = 0; i < vStrings.size(); ++i)
cout << vStrings[i]->c_str() << "\n";
Normally, in the above code, the desired logic is to sort the objects in the container, but with most smart pointers, the object address is sorted instead. The result is that the output comes out unsorted based on the names.
With a clone_ptr
, the comparison is made on the object being pointed to and not on the address of the pointer. The following code will output the names sorted.
vector<clone_ptr<string> > vStrings;
vStrings.push_back(new string("Jill"));
vStrings.push_back(new string("Tom"));
vStrings.push_back(new string("David"));
vStrings.emplace_back(new string("Zulma"));
vStrings.emplace_back(new string("Yari"));
vStrings.emplace_back(new string("Bill"));
sort(vStrings.begin(), vStrings.end()); for (size_t i = 0; i < vStrings.size(); ++i)
cout << vStrings[i]->c_str() << "\n";
In a container of smart pointers, it's rarely required, nor desired to make a comparison on the pointer address, and instead, it's more beneficial and practical to have the operators apply logic to the deference pointer, instead of the pointer itself. That's the main reason why the clone_ptr
class does not try to do a 100% pointer emulation, and instead it's more of a hybrid in which it emulates pointers where it's beneficial, but emulates referencing variable types when applying comparison operators.
Although the clone_ptr
class was specifically designed for use with STL containers, it can still be useful and has practical applications outside of an STL container.
Using the Code
There are many benefits to using the clone_ptr
over other smart pointers, but the following highlights some of the main benefits.
The clone_ptr
allows for the copying of an object, instead of copying the pointer address.
void CopyCorrectDerivedTypeDemo() {
std::vector < clone_ptr < BaseClass> > vBaseClass;
vBaseClass.push_back(new BaseClass( "1" ));
vBaseClass.push_back(new Derived_B( "2" ));
vBaseClass.push_back(new BaseClass( "3" ));
vBaseClass.push_back(new Derived_A( "4" ));
vBaseClass.push_back(new BaseClass( "5" ));
std::vector < clone_ptr < BaseClass > >
vBaseClass_Copy(vBaseClass.begin(), vBaseClass.end());
for (std::vector < clone_ptr < BaseClass > >::size_type i
= 0;i < vBaseClass_Copy.size();++i)
{
vBaseClass_Copy[i]->WhoAmI();
}
When sorting a container of clone_ptr
, the object type is sorted instead of the pointer address.
void VectorSortDemo()
{
std::vector < clone_ptr < BaseClass> > vBaseClass;
vBaseClass.push_back(new BaseClass( "3" ));
vBaseClass.push_back(new Derived_B( "2" ));
vBaseClass.push_back(new BaseClass( "1" ));
vBaseClass.push_back(new Derived_A( "5" ));
vBaseClass.push_back(new BaseClass( "4" ));
sort(vBaseClass.begin(), vBaseClass.end());
for (std::vector < clone_ptr < BaseClass > >::size_type
i = 0;i < vBaseClass.size();++i)
{
vBaseClass[i]->WhoAmI();
}
std::set
and std::map
containers also get correctly sorted when using the clone_ptr
.
void SetDemo()
{
std::set < clone_ptr < BaseClass > > sBaseClass;
sBaseClass.insert(new BaseClass( "3" ));
sBaseClass.insert(new Derived_B( "2" ));
sBaseClass.insert(new BaseClass( "1" ));
sBaseClass.insert(new Derived_A( "5" ));
sBaseClass.insert(new BaseClass( "4" ));
for(std::set < clone_ptr < BaseClass > >::iterator i =
sBaseClass.begin();i!=sBaseClass.end();++i)
{
(*i)->WhoAmI();
}
The equality operator, assists in determining if one container is equal to another.
void ContainerEqualityDemo()
{
list < clone_ptr < BaseClass > > PtrsX;
PtrsX.push_back(new BaseClass("1"));
PtrsX.push_back(new BaseClass("2"));
PtrsX.push_back(new BaseClass("3"));
list < clone_ptr < BaseClass > > PtrsY;
PtrsY.push_back(new Derived_B("4"));
PtrsY.push_back(new Derived_B("5"));
PtrsY.push_back(new Derived_B("6"));
list < clone_ptr < BaseClass > > PtrsZ;
PtrsZ.push_back(new Derived_A("1"));
PtrsZ.push_back(new Derived_A("2"));
PtrsZ.push_back(new Derived_A("3"));
bool IsEqual1 = (PtrsX == PtrsY); bool IsEqual2 = (PtrsX == PtrsZ);
cout << "Should be false (0) IsEqual1 = " << IsEqual1 << endl;
cout << "Should be true (1) IsEqual2 = " << IsEqual2 << endl;
The equality operator also helps with STL replace and remove functions.
void RemoveFromListDemo()
{
std::list < clone_ptr < BaseClass> > lBaseClass;
lBaseClass.push_back(new BaseClass( "X" ));
lBaseClass.push_back(new Derived_B( "2" ));
lBaseClass.push_back(new BaseClass( "X" ));
lBaseClass.push_back(new Derived_A( "5" ));
lBaseClass.push_back(new BaseClass( "4" ));
lBaseClass.remove(clone_ptr < BaseClass > (new BaseClass("X")));
for(std::list < clone_ptr < BaseClass > >::iterator
i = lBaseClass.begin();i!=lBaseClass.end();++i)
{
(*i)->WhoAmI();
}
Pros and Cons
Cons
When coping a container, the shared_ptr
may use less memory than the clone_ptr
because it copies the address, instead of coping the object.
Pros
When objects are copied in a container, unique copies are created, which can be manipulated without effecting the original object.
Produces expected comparison behavior, because the comparison operators are performed on the object, and not the pointer.
Can be used in std::map
and std::set
to get sorted results based on the object.
Works with std::sort
and produces expected behavior.
Conclusions
By using the clone_ptr
, you get the advantages of a pointer and smart pointers, without the disadvantages of the sort issues and not being able to create independent copies.
All the above code is in the demo project listed for download.
History
- 21st July, 2021
- Updated usage examples
- Added pros & cons
- Added clarification that the main issue is reference logic over pointer logic