std::unique_ptr

The Spirit Of std::unique_ptr

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

  • No simple pointer copy allowed

    • ⟶ would create second reference

  • Explicit ownership transfer needed ⟶ Move-only

  • Implicit ownership transfer only 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

Creating std::unique_ptr Instances

  • Raw pointer at the basis

  • std::unique_ptr<> wrapped around

  • ⟶ controlled/deterministic deallocation

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, basic_creation)
{
    BigData* data = new BigData(100, 'a');
    std::unique_ptr<BigData> pdata(data);
}
  • More condensed usage: std::make_unique<T>()

  • Convenience function that

    • allocates T, using new

    • forwards its arguments to T’s constructor

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, more_condensed_creation)
{
    auto pdata = std::make_unique<BigData>(100, 'a');
}

Inhibited: Copying std::unique_ptr Instances

  • Uniqueness: no two pointers can point to the same object

  • ⟶ Copy constructor (and assignment operator) are deleted

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, copy_inhibited)
{
    auto pdata = std::make_unique<BigData>(100, 'a');
    auto copy = pdata;                                 // <-- copy initializer inhibited
}
  • Compiler error: copy constructor is deleted

  • (Same with copy assignment)

error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = BigData; _Dp = std::default_delete<BigData>]’
    8 |     auto copy = pdata;                                 // <-- copy initializer inhibited
      |                 ^~~~~

Inhibited: Pass By Copy

  • Consequence: pass-by-copy is also inhibited

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

static void foo(std::unique_ptr<BigData> d)            // <-- pass by copy
{
    // ... do something with p
}

TEST(unique_ptr_suite, pass_by_copy_inhibited)
{
    auto pdata = std::make_unique<BigData>(100, 'a');
    foo(pdata);                                        // <-- copy initializer inhibited
}
error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = BigData; _Dp = std::default_delete<BigData>]’
   13 |     foo(pdata);                                        // <-- copy initializer inhibited
      |     ~~~^~~~~~~

Passing Temporary Objects Are Moved Automatically

  • Compiler knows that a temprary object has no name

  • ⟶ Cannot be known to anyone but the compiler

  • ⟶ Compiler can do with it what he wants

  • Move it into the function’s parameter (see here)

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

static void foo(std::unique_ptr<BigData> d)
{
    // ... do something with p
}

TEST(unique_ptr_suite, temporary_object_moved)
{
    foo(std::make_unique<BigData>(100, 'a'));          // <-- std::unique_ptr move ctor called
}

std::move(): Moving Objects Explicitly

  • Developer might decide that they don’t need a variable anymore

  • ⟶ Explicit move using std::move()

  • Moved-from object is in invalid state afterwards

  • In case of std::unique_ptr: nullptr

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

static void foo(std::unique_ptr<BigData> d)            // <-- pass by copy
{
    // ... do something with p
}

TEST(unique_ptr_suite, pass_by_copy_explicit_move)
{
    auto pdata = std::make_unique<BigData>(100, 'a');
    foo(std::move(pdata));                             // <-- explicit ownership transfer
    ASSERT_EQ(pdata, nullptr);                         // <-- pdata **INVALID** after move
}

Object Access: std::unique_ptr Behaves Like A Pointer

  • Has overloaded operators that make it behave like a pointer

  • operator->()

  • operator*()

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, is_a_pointer)
{
    auto pdata = std::make_unique<BigData>(100, 'a');
    ASSERT_EQ(pdata->at(42), 'a');
    ASSERT_EQ((*pdata).at(42), 'a');
}

Raw Pointer Access: .get()

  • Not the plan

  • Sometimes needed though

  • ⟶ Careful!

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, raw_pointer_get)
{
    auto pdata = std::make_unique<BigData>(100, 'a');
    BigData* raw = pdata.get();
    ASSERT_EQ(raw->at(42), 'a');                       // <-- operator->()
    ASSERT_EQ((*raw).at(42), 'a');                     // <-- operator*()
    ASSERT_EQ(raw, pdata.get());                       // <-- .get() leaves pdata untouched
}

Re-Gaining Control: .release()

  • Return pointed-to object to caller

  • Modifies std::unique_ptr object to not point to anything

  • Caller now responsible for object deallocation

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, raw_pointer_release)
{
    auto pdata = std::make_unique<BigData>(100, 'a');
    BigData* raw = pdata.release();                    // <-- developer is now responsible for `raw`
    ASSERT_EQ(pdata, nullptr);
    ASSERT_EQ(raw->at(42), 'a');
    delete raw;                                        // <-- (here)
}

Deallocation Before Pointer Destruction: .reset()

  • Nulling out pointer object

  • With all consequences: pointed-to object deallocated

  • Pointer object still alive but invalid

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, reset)
{
    auto pdata = std::make_unique<BigData>(100, 'a');
    pdata.reset();                                     // <-- deallocate managed object
    ASSERT_EQ(pdata, nullptr);
}
  • Variant: exchanging pointer-to object with different (or even same) object

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, reset_exchange)
{
    auto pdata = std::make_unique<BigData>(100, 'a');
    BigData* newdata = new BigData(1000, 'b');
    pdata.reset(newdata);                              // <-- deallocate, and point to new object
    ASSERT_EQ(pdata.get(), newdata);
}

Custom Deleter

  • Additional template parameter Deleter

    • Callable

    • Signature void(T*)

  • Additional constructor parameter of that type

#include "big-data.h"
#include <gtest/gtest.h>
#include <memory>

TEST(unique_ptr_suite, deleter)
{
    bool deleted = false;
    auto deleter = [&deleted](BigData* obj) {
        deleted = true;
        delete obj;
    };

    std::unique_ptr<BigData, decltype(deleter)> pdata(new BigData(100, 'a'), deleter);
    pdata.reset();
    ASSERT_TRUE(deleted);
}