std::unique_ptr

The Spirit Of std::unique_ptr

  • Unique resource ownership: only one pointer object is responsible for deleting the referenced object

  • No simple pointer copy allowed

  • ⟶ would create second reference

  • Explicit ownership transfer needed

  • Only implicit when compiler can prove that no harm is done

  • unique_ptr is only one (though important) user of a new language feature: Move Semantics, Rvalue References, Perfect Forwarding

Methods

Object of type std::unique_ptr behave like pointers in any respect (->, *, copy, …), except that they have methods:

Method

Description

std::unique_ptr()

Initializes to nullptr

std::unique_ptr(T* pointer)

Initializes to pointer (but see std::make_unique instead)

std::unique_ptr(std::unique_ptr&& from)

Move constructor; from is empty afterwards (see Move Semantics, Rvalue References, Perfect Forwarding)

release()

Returns pointer to managed object, and releases ownership

reset(T* pointer)

Takes ownership of pointer (can be nullptr), and deletes currently owned pointer (if any)

get()

Pointer to currently owned object

Basic Usage: Prevent Leaks

#include "sensors.h"

#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, basic)
{
    std::map<std::string, std::unique_ptr<Sensor>> sensors;
    
    RandomSensor* rs1 = new RandomSensor{20, 40};
    RandomSensor* rs2 = new RandomSensor{10, 30};
    ConstantSensor* cs = new ConstantSensor{36.5};

    // if this threw we'd leak memory of sensor objects
    auto temp = rs1->get_temperature();
    ASSERT_TRUE(temp>=20 && temp<=40);

    sensors["rand1"] = std::unique_ptr<Sensor>{rs1};
    sensors["rand2"] = std::unique_ptr<Sensor>{rs2};
    sensors["const"] = std::unique_ptr<Sensor>{cs};
}
  • All looks well: pointers safely wrapped in std::unique_ptr<>

  • ⟶ most basic intention

  • It’s just that little chance that something might throw between allocation and wrap

Eliminate Chance Of Leakage ⟶ Ownership

Straightforward try to prevent leaks:

#include "sensors.h"

#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, ownership_error)
{
    std::map<std::string, std::unique_ptr<Sensor>> sensors;
    
    std::unique_ptr<Sensor> rs1{new RandomSensor{20, 40}};
    std::unique_ptr<Sensor> rs2{new RandomSensor{10, 30}};
    std::unique_ptr<Sensor> cs{new ConstantSensor{36.5}};

    auto temp = rs1->get_temperature();
    ASSERT_TRUE(temp>=20 && temp<=40);

    sensors["rand1"] = rs1;
    sensors["rand2"] = rs2;
    sensors["const"] = cs;
}
  • Apparently cannot simply assign one unique pointer onto another

  • Remember: one single owner

  • ⟶ assignment would create a second

  • ⟶ compiler helps us!

/home/jfasch/work/jfasch-home/trainings/material/soup/cxx11/030-smart-pointers/code/unique-ptr-ownership-error.cpp: In member function ‘virtual void unique_ptr_suite_ownership_error_Test::TestBody()’:
/home/jfasch/work/jfasch-home/trainings/material/soup/cxx11/030-smart-pointers/code/unique-ptr-ownership-error.cpp:17:24: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>& std::unique_ptr<_Tp, _Dp>::operator=(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Sensor; _Dp = std::default_delete<Sensor>]’
   17 |     sensors["rand1"] = rs1;
      |                        ^~~
In file included from /usr/include/c++/11/memory:76,
                 from /home/jfasch/work/jfasch-home/googletest/googletest/include/gtest/gtest.h:54,
                 from /home/jfasch/work/jfasch-home/trainings/material/soup/cxx11/030-smart-pointers/code/unique-ptr-ownership-error.cpp:3:
/usr/include/c++/11/bits/unique_ptr.h:469:19: note: declared here
  469 |       unique_ptr& operator=(const unique_ptr&) = delete;
      |                   ^~~~~~~~
  • ⟶ Copy not possible!

  • Who would be responsible to destroy the object?

  • Ownership violation

Saving Keystrokes: std::make_unique<>()

  • Too much typing here:

    #include "sensors.h"
    
    #include <gtest/gtest.h>
    #include <memory>
    
    TEST(unique_ptr_suite, verbose_ctor)
    {
        std::unique_ptr<Sensor> rs1{new RandomSensor{20, 40}};
        std::unique_ptr<Sensor> rs2{new RandomSensor{10, 30}};
        std::unique_ptr<Sensor> cs{new ConstantSensor{36.5}};
    }
    
  • std::make_unique to the rescue:

    #include "sensors.h"
    
    #include <gtest/gtest.h>
    #include <memory>
    
    TEST(unique_ptr_suite, make_unique_auto)
    {
        auto rs1 = std::make_unique<RandomSensor>(20, 40);
        auto rs2 = std::make_unique<RandomSensor>(10, 30);
        auto cs = std::make_unique<ConstantSensor>(36.5);
    }
    

Explicitly Acknowledging Ownership Transfer: std::move()

#include "sensors.h"

#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, ownership_error)
{
    std::map<std::string, std::unique_ptr<Sensor>> sensors;
    
    auto rs1 = std::make_unique<RandomSensor>(20, 40);
    auto rs2 = std::make_unique<RandomSensor>(10, 30);
    auto cs = std::make_unique<ConstantSensor>(36.5);

    auto temp = rs1->get_temperature();
    ASSERT_TRUE(temp>=20 && temp<=40);

    // invalidate outside object references, and move objects to be
    // owned by map
    sensors["rand1"] = std::move(rs1);
    sensors["rand2"] = std::move(rs2);
    sensors["const"] = std::move(cs);

    // outside references are gone
    ASSERT_EQ(rs1.get(), nullptr);
    ASSERT_EQ(rs2.get(), nullptr);
    ASSERT_EQ(cs.get(), nullptr);
}

Compiler Can Prove: Implicit Ownership Transfer

  • Function returns an object by copy

  • ⟶ compiler knows that that object will never be used anymore

  • To the caller this object appears as R-Value (something that cannot be assigned to)

  • Enter “R-Value References”

  • unique_ptr has a constructor that can accept an R-Value reference

  • &&

  • “Move-Constructor”

    unique_ptr(unique_ptr&& u) noexcept;
    
#include "sensors.h"

#include <gtest/gtest.h>
#include <memory>

static std::unique_ptr<Sensor> create_random_sensor(double low, double high)
{
    // do some boilerplate (like going through config) before creating
    // sensor object
    return std::make_unique<RandomSensor>(low, high);
}
static std::unique_ptr<Sensor> create_constant_sensor(double temp)
{
    // do some boilerplate (like going through config) before creating
    // sensor object
    return std::make_unique<ConstantSensor>(temp);
}

TEST(unique_ptr_suite, implicit_ownership_transfer)
{
    std::map<std::string, std::unique_ptr<Sensor>> sensors;

    sensors["rand1"] = create_random_sensor(20, 40);
    sensors["rand2"] = create_random_sensor(20, 40);
    sensors["const"] = create_constant_sensor(36.5);
}

So unique_ptr’s move constructor is responsible for handling ownership transfer.

How To Write Code That Can Take Ownership?

  • Ok, unique_ptr is programmed to take ownership if possible

  • What if I have code that wants to take ownership of a unique_ptr (or any type that I can move from, for that matter)?

  • ⟶ Write my own move constructor

#include "sensors.h"

#include <gtest/gtest.h>
#include <memory>

class HoldsASensor
{
public:
    HoldsASensor(std::unique_ptr<Sensor>&& s)
    : _sensor(std::move(s)) {}

private:
    std::unique_ptr<Sensor> _sensor;
};

static std::unique_ptr<Sensor> create_constant_sensor(double temp)
{
    return std::make_unique<ConstantSensor>(temp);
}

TEST(unique_ptr_suite, taking_ownership_in_own_code)
{
    auto has = HoldsASensor(create_constant_sensor(2));
}

Attention

Pitfall alert

HoldsASensor(std::unique_ptr<Sensor>&& s)
: _sensor(std::move(s)) {}

Note that the parameter s is an lvalue, for that matter - it has a name. So std::move() is necessary. std::unique_ptr does not support copy, by nature, so the code won’t sompile anyway. Other types may (std::string, for example). There be dragons!

See Move Semantics, Rvalue References, Perfect Forwarding for more.

Manipulating Pointer Content

Method

Description

T* get()

returns contained pointer

reset(T* = nullptr)

Deletes contained pointer, takes ownership of new pointer

release()

Returns contained pointer, releasing ownership

#include "sensors.h"

#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, release)
{
    auto raw = new RandomSensor{20, 30};
    auto ptr = std::unique_ptr<Sensor>(raw);

    auto raw1 = ptr.release();

    ASSERT_EQ(raw1, raw);
    ASSERT_EQ(ptr.get(), nullptr);

    // me being owner now!
    delete raw1;
}

TEST(unique_ptr_suite, reset)
{
    std::unique_ptr<Sensor> ptr = std::make_unique<RandomSensor>(20, 30);

    ptr.reset(new ConstantSensor{15}); // raw1 gone
    ptr.reset();  // all gone
    
    ASSERT_EQ(ptr.get(), nullptr);
}