strong_typedef - Create distinct types for distinct purposes
Wednesday, 29 May 2019
One common problem in C++ code is the use of simple types for many things: a
std::string
might be a filename, a person's name, a SQL query string or a
piece of JSON; an int
could be a count, an index, an ID number, or even a file
handle. In his 1999 book "Refactoring" (which has a
second edition as of January 2019), Martin Fowler called this phenomenon
"Primitive Obsession", and recommended that we use dedicated classes for each
purpose rather than built-in or library types.
The difficulty with doing so is that built-in types and library types have predefined sets of operations that can be done with them from simple operations like incrementing/decrementing and comparing, to more complex ones such as replacing substrings. Creating a new class each time means that we have to write implementations for all these functions every time. This duplication of effort raises the barrier to doing this, and means that we often decide that it isn't worthwhile.
However, by sticking to the built-in and library types, we can end up in a scenario where a function takes multiple parameters of the same type, with distinct meanings, and no clear reason for any specific ordering. In such a scenario, it is easy to get the parameters in the wrong order and not notice until something breaks. By wrapping the primitive type in a unique type for each usage we can eliminate this class of problem.
My strong_typedef
class
template aims to make this easier. It wraps an existing type, and associates it
with a tag type to define the purpose, and which can therefore be used to make
it unique. Crucially, it then allows you to specify which sets of operations you
want to enable: it might not make sense to add ID numbers, but it might make
perfect sense to add counters, even if both are represented by integers. You
might therefore using jss::strong_typedef<struct IdTag,unsigned,jss::strong_typedef_properties::equality_comparable>
for an ID
number, but jss::strong_typedef<struct IndexTag,unsigned,jss::strong_typedef_properties::comparable | jss::strong_typedef_properties::incrementable | jss::strong_typedef_properties::decrementable>
for an index type.
I've implemented something similar to this class for various clients over the years, so I decided it was about time to make it publicly available. The implementation on github condenses all of the solutions to this problem that I've written over the years to provide a generic implementation.
Basic Usage
jss::strong_typedef
takes three template parameters: Tag
, ValueType
and Properties
.
The first (Tag
) is a tag type. This is not used for anything other than to
make the type unique, and can be incomplete. Most commonly, this is a class or
struct declared directly in the template parameter, and nowhere else, as in the
examples struct IdTag
and struct IndexTag
above.
The second (ValueType
) is the underlying type of the strong typedef. This
is the basic type that you would otherwise be using.
The third (Properties
) is an optional parameter that specifies the operations
you wish the strong typedef to support. By default it is
jss::strong_typedef_properties::none
— no operations are supported. See
below for a full list.
Declaring Types
You create a typedef by specifying these parameters:
using type1=jss::strong_typedef<struct type1_tag,int>;
using type2=jss::strong_typedef<struct type2_tag,int>;
using type3=jss::strong_typedef<struct type3_tag,std::string,
jss::strong_typedef_properties::comparable>;
type1
, type2
and type3
are now separate types. They cannot be implicitly converted
to or from each other or anything else.
Creating Values
If the underlying type is default-constructible, then so is the new type. You can also construct the objects from an object of the wrapped type:
type1 t1;
type2 t2(42);
// type2 e2(t1); // error, type1 cannot be converted to type2
Accessing the Value
strong_typedef
can wrap built-in or class type, but that's only useful if you
can access the value. There are two ways to access the value:
- Cast to the stored type:
static_cast<unsigned>(my_channel_index)
- Use the
underlying_value
member function:my_channel_index.underlying_value()
Using the underlying_value
member function returns a reference to the stored
value, which can thus be used to modify non-const
values, or to call member
functions on the stored value without taking a copy. This makes it particularly
useful for class types such as std::string
.
using transaction_id=jss::strong_typedef<struct transaction_tag,std::string>;
bool is_a_foo(transaction_id id){
auto& s=id.underlying_value();
return s.find("foo")!=s.end();
}
Other Operations
Depending on the properties you've assigned to your type you may
be able to do other operations on that type, such as compare a == b
or
a < b
, increment with ++a
, or add two values with a + b
. You might also be
able to hash the values with std::hash<my_typedef>
, or write them to a
std::ostream
with os << a
. Only the behaviours enabled by the Properties
template parameter will be available on any given type. For anything else, you
need to extract the wrapped value and use that.
Examples
IDs
An ID of some description might essentially be a number, but it makes no sense
to perform much in the way of operations on it. You probably want to be able to
compare IDs, possibly with an ordering so you can use them as keys in a
std::map
, or with hashing so you can use them as keys in std::unordered_map
,
and maybe you want to be able to write them to a stream. Such an ID type might
be declared as follows:
using widget_id=jss::strong_typedef<struct widget_id_tag,unsigned long long,
jss::strong_typedef_properties::comparable |
jss::strong_typedef_properties::hashable |
jss::strong_typedef_properties::streamable>;
using froob_id=jss::strong_typedef<struct froob_id_tag,unsigned long long,
jss::strong_typedef_properties::comparable |
jss::strong_typedef_properties::hashable |
jss::strong_typedef_properties::streamable>;
Note that froob_id
and widget_id
are now different types due to the
different tags used, even though they are both based on unsigned long long
. Therefore any attempt to use a widget_id
as a froob_id
or vice-versa
will lead to a compiler error. It also means you can overload on them:
void do_stuff(widget_id my_widget);
void do_stuff(froob_id my_froob);
widget_id some_widget(421982);
do_stuff(some_widget);
Alternatively, an ID might be a string, such as a purchase order number of transaction ID:
using transaction_id=jss::strong_typedef<struct transaction_id_tag,std::string,
jss::strong_typedef_properties::comparable |
jss::strong_typedef_properties::hashable |
jss::strong_typedef_properties::streamable>;
transaction_id some_transaction("GBA283-HT9X");
That works too, since strong_typedef
can wrap any built-in or class type.
Indexes
Suppose you have a device that supports a number of channels, so you want to be
able to retrieve the data for a given channel. Each channel yields a number of
data items, so you also want to access the data items by index. You could use
strong_typedef
to wrap the channel index and the data item index, so they
can't be confused. You can also make the index types incrementable
and
decrementable
so they can be used in a for
loop:
using channel_index=jss::strong_typedef<struct channel_index_tag,unsigned,
jss::strong_typedef_properties::comparable |
jss::strong_typedef_properties::incrementable |
jss::strong_typedef_properties::decrementable>;
using data_index=jss::strong_typedef<struct data_index_tag,unsigned,
jss::strong_typedef_properties::comparable |
jss::strong_typedef_properties::incrementable |
jss::strong_typedef_properties::decrementable>;
Data get_data_item(channel_index channel,data_index item);
data_index get_num_items(channel_index channel);
void process_data(Data data);
void foo(){
channel_index const num_channels(99);
for(channel_index channel(0);channel<num_channels;++channel){
data_index const num_data_items(get_num_items(channel));
for(data_index item(0);item<num_data_items;++item){
process_data(get_data_item(channel,item));
}
}
}
The compiler will complain if you pass the wrong parameters, or compare the
channel
against the item
.
Behaviour Properties
The Properties
parameter specifies behavioural properties for the new type. It
must be one of the values of jss::strong_typedef_properties
, or a value obtained by or-ing them
together (e.g. jss::strong_typedef_properties::hashable | jss::strong_typedef_properties::streamable | jss::strong_typedef_properties::comparable
). Each
property adds some behaviour. The available properties are:
equality_comparable
=> Can be compared for equality (st==st2
) and inequality (st!=st2
)pre_incrementable
=> Supports preincrement (++st
)post_incrementable
=> Supports postincrement (st++
)pre_decrementable
=> Supports predecrement (--st
)post_decrementable
=> Supports postdecrement (st--
)addable
=> Supports addition (st+value
,value+st
,st+st2
) where the result is convertible to the underlying type. The result is a new instance of the strong typedef.subtractable
=> Supports subtraction (st-value
,value-st
,st-st2
) where the result is convertible to the underlying type. The result is a new instance of the strong typedef.ordered
=> Supports ordering comparisons (st<st2
,st>st2
,st<=st2
,st>=st2
)mixed_ordered
=> Supports ordering comparisons where only one of the values is a strong typedefhashable
=> Supports hashing withstd::hash
streamable
=> Can be written to astd::ostream
withoperator<<
incrementable
=>pre_incrementable | post_incrementable
decrementable
=>pre_decrementable | post_decrementable
comparable
=>ordered | equality_comparable
Guideline and Implementation
I strongly recommend using strong_typedef
or an equivalent implementation
anywhere you would otherwise reach for a built-in or library type such as int
or std::string
when designing an interface.
My strong_typedef
implementation is available on github under the Boost
Software License.
Posted by Anthony Williams
[/ cplusplus /] permanent link
Tags: cplusplus, memory, safety
Stumble It! | Submit to Reddit | Submit to DZone
If you liked this post, why not subscribe to the RSS feed or Follow me on Twitter? You can also subscribe to this blog by email using the form on the left.
Design and Content Copyright © 2005-2024 Just Software Solutions Ltd. All rights reserved. | Privacy Policy
No Comments