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