Dangerous std::shared_ptr
std::shared_ptr is very useful and flexible smart pointer provided by C++ standard library. But we should use it very carefully, because it is very easy to make a mistake in a real project. One of the possible mistakes happened with me recently. Just look at code example below. This code is truncated example of real code from a real project. And this example works as expected.
// Transport level class like pipes, sockets etc.
// Instance of this class is accessed from different places.
class Transport : public std::enable_shared_from_this<Transport>
{
public:
};
// Main class creates a new instance of transport.
class MainClass
{
public:
MainClass()
{
m_transport = std::make_shared<Transport>();
}
public:
const std::shared_ptr<Transport>& GetTransport()
{
return m_transport;
}
private:
std::shared_ptr<Transport> m_transport;
};
// All other classes use transport from MainClass.
class OtherClass
{
public:
OtherClass(MainClass& owner) : _transport(owner.GetTransport())
{
}
private:
std::shared_ptr<Transport> _transport;
};
// usage example
int main()
{
MainClass mainClass;
OtherClass otherClass(mainClass);
}
Later I've refactored the code of MainClass as follows. I had reasons to do this.
class MainClass
{
public:
MainClass()
{
m_transport = std::make_shared<Transport>();
}
public:
Transport* GetTransport()
{
return m_transport.get();
}
private:
std::shared_ptr<Transport> m_transport;
};
Project was compiled successfully without any warnings, but in runtime I've got an error. When we have a small code example then obviously that this code contains a mistake. When we have a big solution from many projects then it is very difficult to catch this mistake. When I've found, and fixed my error I started thinking. In this situation I have three different ways to avoid this type mistakes:
- Be careful. This solution is not for me, because I know that it is much easier said than done.
Replace std::shared_ptr by another smart pointer implementation. This is acceptable for me if I cannot find another solution.
- Generate compile time error for the situation above. In this case I can keep using std::shared_ptr and do not need to rewrite all places, where std::shared_ptr was used.
I examined std::shared_ptr implementation and I found that it uses different internal classes for different ways creation of std::shared_ptr. _Ref_count_obj template class is used for creation through std::make_shared and _Ref_count template class is used for creation from an existing pointer. Currently, it is very easy to create a macro, which can disable one of shared_ptr constructors for a specific class:
// Visual Studio 2012 and higher versions do not use tr1 namespace
#if _MSC_VER >= 1700
#define SHARED_PTR_DISABLE_CREATION_FROM_POINTER(type) namespace std\
{ template<> class _Ref_count<type> {};}
#else
#define SHARED_PTR_DISABLE_CREATION_FROM_POINTER(type) namespace std\
{ namespace tr1 { template<> class _Ref_count<type> {}; } }
#endif
This macro defines an empty template specialization of _Ref_count for a specified type. If the template specialization is used then compile time error will be generated. Currently to check that std::shared_ptr for class Transport is used properly I need only one additional line:
class Transport : public std::enable_shared_from_this<Transport>
{
public:
};
SHARED_PTR_DISABLE_CREATION_FROM_POINTER(Transport)
So, I think that it is not ideal solution and it will be better to replace std::shared_ptr by another smart pointer implementation. From opposite side many people know std::shared_ptr and with help of this trick we can avoid of mistakes. I check that my macro works properly with VS2010, VS2012, VS2013 and VS2015, but I can not guarantee that it will work for another versions of Visual Studio, because it is based on internal implementation of Microsoft.
Interesting thoughts on this topic - https://stackoverflow.com/questions/37377588/was-raw-pointer-constructor-of-shared-ptr-a-mistake
What is the reason to use Transport* GetTransport() signature? Taking raw pointer from shared_ptr X and warping it into another shared_ptr Y create a new smart pointer without any link X <-> Y. C++ protects you from mixing raw pointers and smart pointers. The only way you could mix raw pointers with shared_ptr is initialize or reset shared_ptr with a new created (dettached) raw pointer.