std::unique_ptr primer: explicit ownership at interface
(JP 21/08/2015 originally presented reviewing an interface improvment in real code)
In C++98, memory management is mostly a matter of convention: There is no difference between an owning pointer that needs to be deleted, and a non owning pointer that should not be deleted when it is not longer used.
C++11 introduced std::shared_ptr and std::unique_ptr to address this issue.
- unique_ptr should be prefered to shared_ptr when shared ownership is not required.
- unique_ptr needs to be used instead of shared_ptr when ownership transfer is required.
Because of its responsibility of being a unique owner, std::unique_ptr is can only be moved and not copied. This can make its use cumbersome.
We will see an example of API doing transfer of ownership using unique_ptr. This example show how the call sites are dramatically improved and classic memory managment error are eliminated.
//
// Demo using std::unique_ptr to improve a C++03 interface taking owning
// raw pointers (without improving the implementation for now)
//
// The new interface is the wrapper templace function ObjectManager::AddObject
// around the existing overloaded interface that uses raw owning pointer
//
#include <vector>
#include <memory>
#include <assert.h>
class BaseObject;
class ObjectManager;
std::unique_ptr< ObjectManager > sm_manager = nullptr;
////////////////////////////////////////////////////////////////////////////////
//
// Managed Object classes
//
////////////////////////////////////////////////////////////////////////////////
enum class Setting {All, Nothing};
class BaseObject{
public:
virtual ~BaseObject() {}
};
class DerivedObject : public BaseObject
{
public:
void SetSetting( Setting setting ) {}
};
////////////////////////////////////////////////////////////////////////////////
//
// Object managment with explicit lifetime.
// AddObject gain safe overload
//
////////////////////////////////////////////////////////////////////////////////
class ObjectManager
{
public:
// New explicit interface
template< typename ObjectT >
ObjectT& AddObject( std::unique_ptr< ObjectT > object )
{
// Enforced:
// if throw, object is destroyed
// otherwise the memory is now managed by ObjectManager
assert( object );
AddObject( object.get() );
return *object.release(); // succeed: Now managed by ObjectManager
}
// Legacy interface (deprecated)
void AddObject( BaseObject* object )
{
// Understood convention:
// if throw: caller needs to delete siteObj.
// otherwise the memory is now managed by ObjectManager.
CheckValid( object );
m_objects.push_back( object );
}
// Legacy memory managment
~ObjectManager()
{
for ( auto obj : m_objects )
{
delete obj;
}
}
private:
void CheckValid( BaseObject* object )
{
// throw if not valid
}
private:
// Legacy memory managment
std::vector< BaseObject* > m_objects;
};
void DoSomethingThatMayThrow()
{
// something that may throw
}
////////////////////////////////////////////////////////////////////////////////
//
// Example with legacy interface.
//
////////////////////////////////////////////////////////////////////////////////
// OK use of the raw pointer interface.
void AddDerivedManualManagment()
{
DerivedObject* object = new DerivedObject();
try
{
sm_manager->AddObject( object );
}
catch ( ... )
{
delete object;
throw;
}
}
// Incorrect use of the raw pointer interface.
void AddDerivedManualManagmentUnsafe()
{
DerivedObject* object = new DerivedObject();
try
{
sm_manager->AddObject( object );
DoSomethingThatMayThrow(); // if this throw object will be deleted twice.
}
catch ( ... )
{
delete object;
throw;
}
}
// Incorrect use of std::unique_ptr relase()
//
// unique_ptr relase() indicate manual memory management and is dangerous.
// Prefer delegating this to dedicated function like the new AddObject
void AddDerivedManualManagmentWithUniquePtrUnsafe()
{
auto object = std::make_unique< DerivedObject >();
sm_manager->AddObject( object.get() );
DoSomethingThatMayThrow(); // if this throw object will be deleted twice.
object.release();
}
////////////////////////////////////////////////////////////////////////////////
//
// Example with new explicit interface with unique_ptr.
//
////////////////////////////////////////////////////////////////////////////////
void AddDerivedSimple()
{
sm_manager->AddObject(
std::make_unique< DerivedObject >()
);
DoSomethingThatMayThrow();
}
void AddDerivedWithSettingBefore( Setting setting )
{
auto object = std::make_unique< DerivedObject >();
object->SetSetting( setting );
sm_manager->AddObject(
std::move( object ) // object can only be assigned or destroyed after
// this or Undefined Behavior
);
DoSomethingThatMayThrow();
}
void AddDerivedWithSettingBeforeNoMove( Setting setting )
{
// This lambda could also be a normal named function or use capture.
auto makeDerivedWithSetting = []( Setting setting )
{
auto object = std::make_unique< DerivedObject >();
object->SetSetting( setting );
return object;
};
sm_manager->AddObject(
makeDerivedWithSetting( setting ) // No need for std::move
// No unsafe variable left behind
);
DoSomethingThatMayThrow();
}
void AddDerivedWithSettingAfter( Setting setting )
{
DerivedObject& object = sm_manager->AddObject(
std::make_unique< DerivedObject >()
);
object.SetSetting( setting );
DoSomethingThatMayThrow();
}
int main(int argc, char* argv[])
{
sm_manager = std::make_unique< ObjectManager >();
AddDerivedManualManagment();
AddDerivedManualManagmentUnsafe();
AddDerivedManualManagmentWithUniquePtrUnsafe();
AddDerivedSimple();
AddDerivedWithSettingBefore(Setting::All);
AddDerivedWithSettingBeforeNoMove(Setting::All);
AddDerivedWithSettingAfter(Setting::All);
return 0;
}
Presented at IndigoVision during a training session on 21/08/2015.
This work by Jean-Philippe DUFRAIGNE is licensed under a Creative Commons Attribution 4.0 International License.