A Concrete Example of Naming Consistency
One of the common guidelines about naming things in code is to be consistent.
But what does that mean, in practice? I had the chance to be at C++Now this year, and attend Tony Van Eerd’s great talk called Words of Wisdom, where he gave a very practical approach to that question (amongst many other things). And I had the further chance to have a chat there with Tony and Titus Winters and get more insights on the topic.
With this I discovered a new point of view on name consistency, which I’ll present in this article today. All feedback welcome!
Thanks a lot to Tony for reviewing this article.
Types that wrap an object
There are quite a few cases in programming in general, and in C++ in particular, where we want to manipulate a value but wrap in some way in an interface that adds a new meaning to it.
Quite a few of those types have an member function, in charge of accessing the value that they are wrapping. The question is, how to name that member function?
This question constitutes a case study that we can generalize to other situations that can benefit from name consistency. Note that Tony sent out a Twitter survey about this.
To illustrate, let’s start with the example of strong typing. The way I define a strong type is a type that wraps another type to carry specific meaning through its name.
Before delving into naming, here is a quick recap on strong types.
Strong types
One of the many usages of strong types is to handle IDs in code. Say that in your system, an ID is essentially an int
. But int
doesn’t carry a lot of meaning, and a specific type SeatId
makes more sense than int
if you’re developing a booking system for a cinema for instance.
What’s more, using a specific SeatId
type allows to disambiguate types in an interface. Consider the following interface:
Reservation makeReservation(SeatId seatId, FilmId filmId);
This interface makes it hard for you to mix up the parameters by accident and pass the filmId
first, because it wouldn’t compile. While with an interface with raw int
s:
Reservation makeReservation(int seatId, int filmId);
There is more risk of mixing up the parameters because the compiler has no idea how to differentiate a seat ID from a film ID, and wouldn’t stop you from booking a reservation with inconsistent data.
To illustrate strong typing in C++, let’s use the NamedType
library. NamedType
essentially defines a generic type that wraps another, lower-level, type T
:
template <typename T, typename Parameter> class NamedType { public: explicit NamedType(T const& value) : value_(value) {} explicit NamedType(T&& value) : value_(std::move(value)) {} T& get() { return value_; } T const& get() const {return value_; } private: T value_; };
For example, here is how we would define two different types SeatId
and FilmId
with NamedType
:
using SeatId = NamedType<int, struct SeatIdTag>; using FilmId = NamedType<int, struct FilmIdTag>;
How to name the method?
Now that we’re up to speed on strong typing, let’s focus on the name of the method that retrieves the underlying value. In the interface of NamedType
, it happens to be called get()
:
template <typename T, typename Parameter> class NamedType { public: explicit NamedType(T const& value) : value_(value) {} explicit NamedType(T&& value) : value_(std::move(value)) {} T& get() { return value_; } T const& get() const {return value_; } private: T value_; };
But is get()
really a good name? Let’s look at that question through the lens of name consistency.
Smart pointers
To be consistent, you need at least two things to compare, right? So let’s compare our strong type class with another class that share some of its semantics.
The example that Tony takes for this comparison is smart pointers (not entirely clear on smart pointers yet? Check out the Smart developers use Smart pointers series, that starts from the basics of smart pointers and gets to the advanced stuff).
Granted, smart pointers such as std::unique_ptr
for instance don’t have the same semantics as strong types. They wrap a pointer and handle its life cycle, while NamedType
wrap a value to tack a name onto it.
But they do have something in common: they wrap a value, and they both have a way to retrieve that underlying value from their interface.
And that member function for smart pointers is named… get()
! Right on.
.get()
sounds like danger
The purpose of a smart pointer is to relieve you from memory management, and smart pointers came along because memory management isn’t an easy thing to get right all the time. And even when we do get it right, it leads to code that gets in the way and pollutes business code with technical concerns.
Smart pointers offer an interface that strives to be as transparent as possible. You can access members of the underlying pointer with operator->
, get a reference to the pointed value with operator*
, and even put a smart pointer in an if statement because of its conversion to bool
. All this should be enough to use a smart pointer.
The get()
method, on the other hand, allows to get the raw pointer inside of the smart pointer. If you’re calling .get()
, it means that you don’t want to play by the rules of the smart pointer. For some reason, you want access to the raw pointer. And that sounds dangerous.
Indeed, after you call it on get()
, the smart pointer doesn’t know what will happen to the underlying pointer. If you delete
the pointer, it would leads to a double delete
because the smart pointer would call delete
in its destructor anyway.
Note that it is possible to take the ownership of the pointer away from the smart pointer, with the .release()
method (even though someone else should now worry about deleting the pointer). But this says a clearer message to the smart pointer, that is, “you are no longer responsible for this pointer”. And as a result, the smart pointer won’t delete
the pointer in its destructor. Whereas .get()
is more like: “would you hand that pointer to me for a moment, please? But I can’t tell you what I’ll do with it”.
Looking for signs during code review
Does this mean that calling .get()
is necessarily a bad thing in itself? Not always. Sometimes there is a good reason, such calling a C-style interface that only accepts pointers (now is it a good thing that an interface only accepts pointers, perhaps not, but there are some interfaces out there that we can’t change).
But it’s a warning sign. This is something you want to pay attention to during code review. Every time you see a .get()
called, there should be a good reason for it. And there may well be one, but it’s worth checking, if only with quick look around the code.
As a result, your eyes become trained to look for the .get()
s in code, and .get()
takes a special meaning to you. And for this to be efficient, this meaning of .get()
should be the same across the classes that expose it. In other terms, it should be consistent.
Naming consistency
Naming consistency here consists in making sure that the semantics of NamedType
‘s get()
don’t conflict with those of std::unique_ptr
‘s get()
, for example. So, with regard to this special meaning of .get()
, that is, offering a risky access to the underlying resource, does that fit with the NamedType
interface?
Indeed, if you consider that retrieving the underlying value is, like for smart pointers, NOT the by-default operation to do on a strong type, then also calling it .get()
gives it two benefits in terms of consistency.
The first benefit is that reading its interface reminds of the interface of standard smart pointers, where .get()
means unconventional access. So we don’t have to learn this again.
A second benefit of consistency happens during code reviews, because the habit you gained while reviewing code using smart pointers will work here on strong types just as well.
Note that there are other possible names to express that accessing an underlying value is not the normal case, and a risky thing to do. For example, Tony proposes .unsafe()
to make it obvious in code.
On the other hand, if you consider than retrieving the value of the underlying type IS the right way to go about strong types, then calls to .get()
s should pop up in code. And those constitute as many red herrings when reviewing the code because of the previous habit, which makes the review harder. Indeed this not consistent with the .get()
of smart pointers. To be consistent we should call the method differently then.
The right name?
Our point here is to discuss naming consistency and not how to use strong types, but for the sake of the argument let’s assume that you do consider that accessing the underlying value is the normal usage for strong types. In this case, as explained above, get()
wouldn’t be such a good name.
How should we call that method then?
.value()
? Talking about name consistency, it’s not consistent with std::optional
where .value()
can fail and throw an exception if the optional is empty. But maybe this is ok, because std::get
works both for std::variant
where it can fail, and for std::tuple
where it can’t. Or is std::get
also inconsistent in that regard?
Or should we go down a level of abstraction, and use a name such as .as_underlying()
or .as<int>
or .unwrap()
?
If you have an opinion on any of those questions, please express it in the comments below!
And if you’d like to read more about Tony’s guidelines on naming, check out his guide on naming.
You may also like
- How to choose good names in your code
- The Right Question for the Right Name
- More Tips On Naming
- Tony’s guide on naming
Share this post!