(Trying To) Handwrite A Smart Pointer Class in C++ Before C++11

Overview

  • Step by step build a “managed pointer” class, SmartPtr<T>

    • ⟶ Takes single ownership

  • Managed objects: sensor of various kinds

Step 1

  • Take ownership

  • Mimick a pointer (->, *)

Step 2

  • Compiler-generated copy bad

  • Crash

  • std::auto_ptr style

    • “steal” from original

    • non-const copy and assignment

Sensors To Manage

This set of hacks builds upon some semi-real-life sensor implementations, RandomSensor and ConstantSensor:

#ifndef CXX11_UNIQUE_PTR_SENSORS_H
#define CXX11_UNIQUE_PTR_SENSORS_H

#include <random>


class Sensor
{
public:
    virtual ~Sensor() {}
    virtual double get_temperature() = 0;
};

class RandomSensor : public Sensor
{
public:
    RandomSensor(double low, double high)
    : _distribution(std::uniform_real_distribution<double>(low, high)) {}

    virtual double get_temperature() 
    {
        return _distribution(_engine);
    }

private:
    std::uniform_real_distribution<double> _distribution;
    std::default_random_engine _engine;
};

class ConstantSensor : public Sensor
{
public:
    ConstantSensor(double temp)
    : _temp(temp) {}

    virtual double get_temperature()
    {
        return _temp;
    }

private:
    double _temp;
};

#endif

Basic Resource Management, Operator Overloading

  • Take ownership

  • Implement -> and * operators

  • Implement const versions of -> and * operators

#include "sensors.h"
#include <gtest/gtest.h>

template <typename T> class SmartPtr
{
public:
    SmartPtr(T* p) : _p(p) {}
    ~SmartPtr() { delete _p; }

    T* operator->() { return _p; }
    const T* operator->() const { return _p; }
    T& operator*() { return *_p; }
    const T& operator*() const { return *_p; }

private:
    T* _p;
};

TEST(handwritten_suite, basic)
{
    SmartPtr<Sensor> s{new ConstantSensor{20}};

    ASSERT_DOUBLE_EQ(s->get_temperature(), 20);
    ASSERT_DOUBLE_EQ((*s).get_temperature(), 20);
}

TEST(handwritten_suite, basic_const)
{
    const SmartPtr<Sensor> s{new ConstantSensor{20}};

    const Sensor* raw_s = s.operator->();
    const Sensor& raw_s_ref = s.operator*();

    (void)raw_s;
    (void)raw_s_ref;
}

Copy Constructor And Assignment Operator (And Default Ctor)

Naive approach

  • Let compiler do the copy

  • ⟶ generally a bad idea

#include "sensors.h"
#include <gtest/gtest.h>

template <typename T> class SmartPtr
{
public:
    SmartPtr() : _p(nullptr) {}
    SmartPtr(T* p) : _p(p) {}
    ~SmartPtr() { delete _p; }

    T* operator->() { return _p; }
    const T* operator->() const { return _p; }
    T& operator*() { return *_p; }
    const T& operator*() const { return *_p; }

private:
    T* _p;
};

TEST(handwritten_suite, copy_ctor_bad)
{
    SmartPtr<Sensor> s1{new ConstantSensor{20}};
    SmartPtr<Sensor> s2{s1};

    ASSERT_DOUBLE_EQ(s1->get_temperature(), 20);
    ASSERT_DOUBLE_EQ(s2->get_temperature(), 20);
}

TEST(handwritten_suite, assignment_operator_bad)
{
    SmartPtr<Sensor> s1{new ConstantSensor{20}};
    SmartPtr<Sensor> s2;
    
    s2 = s1;

    ASSERT_DOUBLE_EQ(s1->get_temperature(), 20);
    ASSERT_DOUBLE_EQ(s2->get_temperature(), 20);
}

Crashes …

$ ./c++11-smartptr --gtest_filter=handwritten_suite.copy_ctor_bad
Running main() from /home/jfasch/work/jfasch-home/googletest/googletest/src/gtest_main.cc
Note: Google Test filter = handwritten_suite.copy_ctor
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from handwritten_suite
[ RUN      ] handwritten_suite.copy_ctor
Segmentation fault (core dumped)
$ ./c++11-smartptr --gtest_filter=handwritten_suite.assignment_operator_bad
Running main() from /home/jfasch/work/jfasch-home/googletest/googletest/src/gtest_main.cc
Note: Google Test filter = handwritten_suite.assignment_operator_bad
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from handwritten_suite
[ RUN      ] handwritten_suite.assignment_operator_bad
Segmentation fault (core dumped)

valgrind to the rescue …

  • Reveals that it’s actually a double free

    • ⟶ At the end of the test case, where both pointers are destructed

  • But this is not so obvious because non-existent vtable is being traversed

$ valgrind ./c++11-smartptr --gtest_filter=handwritten_suite.copy_ctor
... tons of output (because of polymorphism) ...

auto_ptr Style

  • Copy constructor and assignment operator invalidate source pointer

  • ⟶ one has to know!

#include "sensors.h"
#include <gtest/gtest.h>

template <typename T> class SmartPtr
{
public:
    SmartPtr() : _p(nullptr) {}
    SmartPtr(T* p) : _p(p) {}
    ~SmartPtr() { delete _p; }

    SmartPtr(SmartPtr& other)
    : _p(other._p)
    {
        other._p = nullptr;
    }
    SmartPtr& operator=(SmartPtr& other)
    {
        if (this != &other /*self assignment*/) {
            delete _p;
            _p = other._p;
            other._p = nullptr;
        }
        return *this;
    }

    T* operator->() { return _p; }
    const T* operator->() const { return _p; }
    T& operator*() { return *_p; }
    const T& operator*() const { return *_p; }

    const T* get() const { return _p; }

private:
    T* _p;
};

TEST(handwritten_suite, copy_ctor_good_autoptr_style)
{
    SmartPtr<Sensor> s1{new ConstantSensor{20}};
    SmartPtr<Sensor> s2{s1};

    // ASSERT_DOUBLE_EQ(s1->get_temperature(), 20);    // <---- CRASHES, UNEXPECTEDLY?
    ASSERT_EQ(s1.get(), nullptr);
    ASSERT_DOUBLE_EQ(s2->get_temperature(), 20);
}

TEST(handwritten_suite, assignment_operator_good_autoptr_style)
{
    SmartPtr<Sensor> s1{new ConstantSensor{20}};
    SmartPtr<Sensor> s2;
    
    s2 = s1;

    // ASSERT_DOUBLE_EQ(s1->get_temperature(), 20);    // <---- CRASHES, UNEXPECTEDLY?
    ASSERT_EQ(s1.get(), nullptr);
    ASSERT_DOUBLE_EQ(s2->get_temperature(), 20);
}

Explicit move() Method Maybe?

  • Still no constructor possible that does that

  • Maybe add a move() method that is called on the destiation object

  • ⟶ improves readability

  • ⟶ does not help with correctness

#include "sensors.h"
#include <gtest/gtest.h>

template <typename T> class SmartPtr
{
public:
    SmartPtr() : _p(nullptr) {}
    SmartPtr(T* p) : _p(p) {}
    ~SmartPtr() { delete _p; }

    SmartPtr(const SmartPtr&) = delete;
    SmartPtr& operator=(const SmartPtr&) = delete;

    T* operator->() { return _p; }
    const T* operator->() const { return _p; }
    T& operator*() { return *_p; }
    const T& operator*() const { return *_p; }

    const T* get() const { return _p; }

    void move(SmartPtr& other)
    {
        delete _p;
        _p = other._p;
        other._p = nullptr;
    }

private:
    T* _p;
};

TEST(handwritten_suite, explicit_move)
{
    SmartPtr<Sensor> s1{new ConstantSensor{20}};
    SmartPtr<Sensor> s2;
    
    s2.move(s1);

    ASSERT_EQ(s1.get(), nullptr);
    ASSERT_DOUBLE_EQ(s2->get_temperature(), 20);
}

Stop!!!

  • But lets stop here …

  • … and see what std::unique_ptr (and C++11) can do