C++ – Templates Where the Type is a Shared_Ptr

csmart-pointertemplates

When creating template classes in C++, if the type upon which the template will be specialized is intended to be a shared_ptr type, is it better to make T a shared_ptr type or make T a non-pointer type?

For example, which of the below classes is better, FirstExample or SecondExample? Or alternatively, is this entirely context-dependent? If so, can you an example of a context where FirstExample is better and a context where SecondExample is better?

In my case, clarity of usage is a higher priority than performance.

#include <iostream>
#include <memory>

using IntPtr = std::shared_ptr<int>;

template <class T>
class FirstExample
{

public:
    FirstExample( std::shared_ptr<T> value )
    :myData( value ) {}
    std::shared_ptr<T> getData() const { return myData; }

private:
    std::shared_ptr<T> myData;
};

template <class T>
class SecondExample
{

public:
    SecondExample( const T& value )
    :myData( value ) {}

    T getData() const { return myData; }

private:
    T myData;
};



int main(int argc, const char * argv[])
{
    FirstExample<int> firstInstance( std::make_shared<int>( 10 ) );
    IntPtr copyOfFirstInstanceData = firstInstance.getData();

    SecondExample<IntPtr> secondInstance( std::make_shared<int>(10) );
    IntPtr copyOfSecondInstanceData = secondInstance.getData();

    return 0;
}

Best Answer

Let's first adjust the user-defined ctors for equivalence:

  1. Use pass-by-value and move-construction:

    FirstExample( std::shared_ptr<T> value ) : myData(std::move(value)) {}
    SecondExample( T value ) : myData(std::move(value)) {}
    
  2. Use pass-by-reference and copy-construction:

    FirstExample( const std::shared_ptr<T>& value ) : myData(value) {}
    SecondExample( const T& value ) : myData(value) {}
    
  3. Use pass-by-reference and move-/copy-construction:

    FirstExample( const std::shared_ptr<T>& value ) : myData(value) {}
    SecondExample( const T& value ) : myData(value) {}
    FirstExample( std::shared_ptr<T>&& value ) : myData(std::move(value)) {}
    SecondExample( T&& value ) : myData(std::move(value)) {}
    

As an aside, I would change getData too, in order to avoid copyng where not needed:

const T& getData() const { return myData; }
const std::shared_ptr<T>& getData() const { return myData; }

Now, ThirdExample behaves the same as FirstExample:

template<class T> using ThirdExample = SecondExample<std::shared_ptr<T>>;

The advantage to using the first one is less typing and possibly better error-messages.
The advantage to using the second is more flexibility, as you can use another smart-pointer, as long as it offers the proper interface.

But why should you have to decide between either?
As ThirdExample demonstrates, you can implement the second example, and provide the first as a simple alias-template for convenience, just like the standard-library does for std::integer_sequence and std::index_sequence.