Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Clone Smart Pointer (clone_ptr)

4.89/5 (13 votes)
20 Jul 2021CPOL4 min read 2   754  
A smart pointer which acts like a reference variable
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.

C++
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()); //Will NOT sort objects
//Displays unsorted data
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.

C++
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());  //Sorts the object
//Display sorted data based on the object
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.

C++
void CopyCorrectDerivedTypeDemo() {
std::vector < clone_ptr < BaseClass> >  vBaseClass; 
    //Setup data using base and derived type classes
    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" )); 
    
    //Copy contents from one container to another
    std::vector < clone_ptr < BaseClass > >  
      vBaseClass_Copy(vBaseClass.begin(), vBaseClass.end()); 
    
    //Display results
    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.

C++
void VectorSortDemo()
{
    std::vector < clone_ptr < BaseClass> >  vBaseClass; 
    //Setup data using base and derived type classes
    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 Data
    sort(vBaseClass.begin(), vBaseClass.end()); 

    //Display sorted data
    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.

C++
void SetDemo()
{
    std::set < clone_ptr < BaseClass > >  sBaseClass; 
    //Setup data using base and derived type classes
    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" )); 

    //Display sorted data
    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.

C++
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); //Should be false
    bool IsEqual2 = (PtrsX == PtrsZ); //Should be true
    
    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.

C++
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" ));

    //Remove certain values
    lBaseClass.remove(clone_ptr < BaseClass > (new BaseClass("X"))); //Remove all X's
    
    //Display results
    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)