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;
}
Let’s first adjust the user-defined ctors for equivalence:
-
Use pass-by-value and move-construction:
FirstExample( std::shared_ptr<T> value ) : myData(std::move(value)) {} SecondExample( T value ) : myData(std::move(value)) {}
-
Use pass-by-reference and copy-construction:
FirstExample( const std::shared_ptr<T>& value ) : myData(value) {} SecondExample( const T& value ) : myData(value) {}
-
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
.