Avoid too many smart pointers
Too many smart pointers considered evil
The scenario
You’ve got an entry point, say a class program
with a start
and a stop
method. Then you have a class house
, with a kitchen
and a living_room
both sharing the same door
. The job is to instantiate the house
and it’s
dependencies in start
and delete it in stop
. There is also a shed
. It too
has to be instantiated in start
and deleted in stop
.
The anti-pattern
Here is a version of program
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class program {
std::unique_ptr<house> house_;
std::unique_ptr<shed> shed_;
public:
void start() {
auto d = std::make_shared<door>();
auto k = std::make_unique<kitchen>(d);
auto lr = std::make_unique<living_room>(d);
house_ = std::make_unique<house>(std::move(k), std::move(lr));
shed_ = std::make_unique<shed>();
}
void stop() {
house_.reset();
shed_.reset();
}
};
And here is the gist of the required support from the other classes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class door {
// door code here
};
class kitchen {
std::shared_ptr<door> door_;
public:
explicit kitchen(std::shared_ptr<door> d) : door_{ d } {}
// more kitchen code here
};
class living_room {
std::shared_ptr<door> door_;
public:
explicit living_room(std::shared_ptr<door> d) : door_{ d } {}
// more living_room code here
};
class house {
std::unique_ptr<kitchen> kitchen_;
std::unique_ptr<living_room> living_room_;
public:
house(std::unique_ptr<kitchen> k, std::unique_ptr<living_room> l_r)
: kitchen_{ std::move(k) }, living_room_{ std::move (l_r) }
{}
// more living_room code here
};
class shed {
// shed code here
};
The anti-pattern overuses smart pointers.
We use unique_ptr
, but not for the door
. For it we use shared_ptr
because
it’s shared. As the code changes and an object becomes shared there is ripple
of changes. E.g. the kitchen
has to care that the living_room
uses the same
door
.
The objects have additional responsibilities: e.g. house
is also responsible
with managing the living_room
instance lifetime, in addition to it’s core
responsibility.
The claimed usage of smart pointers in the anti-pattern is to address lifetime
issues. However notice how if constructing shed_
fails by throwing an
exception, house_
stays created after exiting start
. Also notice that the
order in which the house_
and the shed_
get destroyed depends on whether
stop
is called or not before the program
is destroyed. These subtle
lifetime issues become a problem in a large program.
Better option
Here is a version of program
:
1
2
3
4
5
6
7
8
9
10
11
12
class program {
std::unique_ptr<buildings> buildings_;
public:
void start() {
buildings_ = std::make_unique<buildings>();
}
void stop() {
buildings_.reset();
}
};
And here is the gist of the required support from the other classes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class door {
// door code here
};
class kitchen {
door & door_;
public:
explicit kitchen(door & d) : door_{ d } {}
// more kitchen code here
};
class living_room {
door & door_;
public:
explicit living_room(door & d) : door_{ d } {}
// more living_room code here
};
class house {
kitchen & kitchen_;
living_room & living_room_;
public:
house(kitchen & k, living_room & l_r)
: kitchen_{ k }, living_room_{ l_r }
{}
// more living_room code here
};
class shed {
// shed code here
};
// and putting them together in a builder/injector class
class buildings {
door door_;
kitched kitchen_;
living_room living_room_;
house house_;
shed shed_;
public:
buildings()
: door_{},
kitchen_{ door_},
living_room_{ door },
house_{ kitchen_, living_room_ },
shed_{}
{}
};
The start
/stop
pattern is bad, you might leave it as such if you need to
treat it as a legacy boundary (e.g. can’t re-write all the code in an
application).
The better option uses smart pointers just once only to deal with the legacy boundary. It will allocate just once.
It makes each component have a clearer defined purpose.
For example the buildings
class is a builder/injector with the
only purpose to manage the lifetime of the aggregated objects.
References
- C++ Class Lifetime
- C++ Class Taxonomy for the builder/injector class
- Article on dependencies