(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)),
_engine(std::random_device()()) {}
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*
operatorsImplement
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