Strong Types
The missing C++ feature that makes your code more readable, safer, and just as fast
C++ is an expressive language, and one of its strongest features is the rich type system. With user defined types we get a powerful set of tools to express code concepts through types, helping to prevent bugs and make software more understandable. However this set of types can break down, especially when dealing with primitive integers, floats, or even higher level types that aren’t carefully disambiguated. We’ll go through how using these weakly defined types can cause issues in API design, derive some solutions to the problem, and show how those solutions can make our code more expressive, harder to break and just as performant when compiled with a reasonable set of optimization flags.
An API Gone Wrong
Consider this simple example where we have, as part of some physics library, the following function.
// Given time0, current position, current velocity, current acceleration, this will
// calculate the position at time time1
float determine_future_position(
float t0, float pos0, float vel0, float acc0, float t1);This should immediately raise red flags as an API design. Not only are there a large number of parameters, all of the parameters are easily swappable. Nothing in the language will complain if callers of this function do something like this
float now = ...;
float position = ...;
float velocity = ...;
float acceleration = ...;
float target_time = ...;
return determine_future_position(position, velocity, acceleration, now, target_time);Unless the user of this API goes in and inspects the call signature carefully, they’ll have no warning about the fact that they inadvertently moved the initial time from arg0 to arg3. No warning, that is, until they run their code and see garbage values generated.
Clang Tidy’s Helpful Checks
One of the first steps that the API writer could have done is to enable Clang Tidy’s bugprone-easily-swappable-parameters check. This check, in its default configuration, will flag if a function has two or more parameters that can be swapped with an implicit conversion. Indeed, Clang Tidy rejects our above example precisely for this issue.
As an aside, if you’re not currently using Clang Tidy for your projects, it is an incredibly powerful tool. In addition to checks like the swappable parameters, it has a whole host of patterns, bugs and inefficiencies that it can automatically diagnose and sometimes fix. Furthermore if a check doesn’t exist, the API is extensible to write new, custom checks that inspect the full AST. I will cover this in a future post.
Now that we have flagged this function as potentially problematic, what can we do about it? One approach would be to try and better define the types of these arguments we are passing in with a type alias.
// Define our custom types
using TimePoint = float;
using Position = float;
using Velocity = float;
using Acceleration = float;
float determine_future_position(
TimePoint t0, Position pos0, Velocity vel0, Acceleration acc0, TimePoint t1);While this is nice from a readability standpoint, since we’re no longer just relying on the argument names to describe what they are, it doesn’t get us any closer to resolving the core issue. Because type aliases in C++ are just that, aliases. In cppreference it states that “[a type alias] does not introduce a new type and it cannot change the meaning of an existing type name.” Essentially all we’ve done is created a new way to say “float” in our program. With this change Clang Tidy still rejects our code since we’re functionally outputting the exact same program with different aliases. Incorrectly ordered args in calling code for our API still passes all checks.
TimePoint now = ...;
Position position = ...;
Velocity velocity = ...;
Acceleration acceleration = ...;
TimePoint target_time = ...;
return determine_future_position(position, velocity, acceleration, now, target_time);Solution 0: Structured Arguments
One way to redesign our API would be to package some of the parameters together into a struct that can be passed into the function.
using TimePoint = float;
using Position = float;
using Velocity = float;
using Acceleration = float;
struct KinematicState
{
Position position;
Velocity velocity;
Acceleration acceleration;
};
float determine_future_position(TimePoint t0, const KinematicState& state, TimePoint t1);This has indeed cleaned up our API and silenced Clang Tidy, however we’ve simply pushed the problem up a level and introduced potential other failure cases.
One potential issue with this new approach is we now no longer are requiring that all of position, velocity and acceleration are provided to our function. For example if we were to do something like the following, we could entirely omit one of these parameters without any compiler warnings at all.
KinematicState s{.position = 10.f, .velocity = 0.5f};
// Forgot to populate acceleration
auto position = determine_future_position(0.f, s, 1.f);Of course, we can enforce that the struct has all members populated by creating a constructor, so it’s impossible to incompletely initialize the struct
struct KinematicState
{
Position position;
Velocity velocity;
Acceleration acceleration;
KinematicState(Position pos, Velocity vel, Acceleration acc) : position{pos}, velocity{vel}, acceleration{acc} {}
};
KinematicState state(10.f, 0.5f, 0.1f);However we’re right back where we started, now with the constructor for KinematicState having 3 easily swappable parameters and again getting flagged by Clang Tidy.
Our Goal Solution
Ideally in our program, Position, Velocity, Acceleration and TimePoint would all be unique types that cannot be interchanged with each other. As such, we need convenient way to create new types with minimal boilerplate, as well as an escape hatch to grab out our underlying data. All of this should also be zero runtime overhead, leaning into C++’s promise of zero overhead abstraction.
An Initial Set of Types
One way we can quickly create new types is by using C++ classes or structs to define a new type for each of our quantities. When working with user-defined types, C++ offers strong typing. If we have a class Foo and a class Bar, even if their implementation is identical we cannot assign a Foo to Bar, compare a Bar against a Foo or mix between them in any way, unless we explicitly write conversions or operators that take in mixed class types. We’ll use this to create our first simple set of types.
struct Position
{
float value;
};
struct Velocity
{
float value;
};
struct Acceleration
{
float value;
};
struct TimePoint
{
float value;
};
float determine_future_position(
TimePoint t0, Position pos0, Velocity vel0, Acceleration acc0, TimePoint t1);This seems to have addressed our problem! We now have a strong type for Position, Velocity, Acceleration and TimePoint. They cannot be swapped with each other, since they are fundamentally different structs.
While this solution works, it is clearly pretty cumbersome. We’ve defined 4 structs, each with an identical body. If we wanted to change anything in our implementation, such as requiring the value is provided upon construction, we’d need to update this in multiple places, which is obviously not ideal. When we see code duplication like this, the first thing to consider should be how we can reduce this, and in C++ that is often achieved by reaching for templates.
Strong Types with Templates
In order to solve this problem with templates, we need to consider how types are created from templates. When we have a templated class, every instantiation of that template with unique template parameters creates a new type, even if that template type isn’t used anywhere within the class! For example in the following, each of FooInt, FooFloat and FooBar are a unique type.
template <typename T>
struct Foo{};
using FooInt = Foo<int>;
using FooFloat = Foo<float>;
struct Bar{};
using FooBar = Foo<Bar>;This example, particularly the last FooBar class should hopefully show the direction we are heading. We can use this information to define our “strong type” class one time, and have each instantiation use a different “tag” to differentiate it as a new type.
template <typename Tag>
struct strong_type
{
private:
float _value;
public:
explicit constexpr strong_type(float value) : _value(value){}
constexpr const float& value() const { return _value; }
constexpr float& value() { return _value; }
};Now that we have this basic strong type defined, we can create new strong types with simple tag structs and some using declarations
struct PositionTag{};
struct VelocityTag{};
struct AccelerationTag{};
struct TimePointTag{};
using Position = strong_type<PositionTag>;
using Velocity = strong_type<VelocityTag>;
using Acceleration = strong_type<AccelerationTag>;
using TimePoint = strong_type<TimePointTag>;We now have a strong typing system available where we can put any float into its own type with a unique tag, creating a new type that cannot be swapped with raw floats or any other strong type besides its own. From here it’s also easy to generalize by adding an additional template parameter T to our strong_type, allowing us to create strong types based on ints, floats, doubles, strings, vectors or really any type that we want.
At this point, we now have a fully usable set of strong types that we can use in our above function. Pulling all of this together, we now have the following piece of code.
template <typename T, typename Tag>
struct strong_type
{
private:
T _value;
public:
explicit constexpr strong_type(T value) : _value(value){}
constexpr const T& value() const { return _value; }
constexpr T& value() { return _value; }
};
struct TimePointTag{};
struct PositionTag{};
struct VelocityTag{};
struct AccelerationTag{};
using TimePoint = strong_type<float, TimePointTag>;
using Position = strong_type<float, PositionTag>;
using Velocity = strong_type<float, VelocityTag>;
using Acceleration = strong_type<float, AccelerationTag>;
float determine_future_position(
TimePoint t0, Position pos0, Velocity vel0, Acceleration acc0, TimePoint t1)
{
const auto time_delta = t1.value() - t0.value();
return pos0.value() + vel0.value() * time_delta + 0.5f * acc0.value() * time_delta * time_delta;
}We use the strong type class to create a unique type based on the tag for each of our parameter types. Note as well that the tags can be hidden away in a detail namespace, they shouldn’t really be accessed once they’ve been used in the type alias.
Our public API is now much more difficult to misuse, since swapping any parameters now causes a compile time error. Within the function body, we use the .value() function to decay down to the underlying type in order to perform the actual calculation. We’ve now succeeded in making a safer and more descriptive API. Indeed the API has become so descriptive from the types used that in the declaration we can potentially drop all the names besides the timepoints since the types themselves describe what the values are, which may simplify the API even more to a reader.
float determine_future_position(TimePoint t0, Position, Velocity, Acceleration, TimePoint t1);Performance Impacts?
Of course, while this has improved the ergonomics for the API user, the question remains how this has affected the actual code that is run. If we’ve slowed down our calculation with the introduction of strong types, that’s clearly an undesirable outcome. To inspect that, we can look at the same function both with strong types, and with the primitive types.
float determine_future_position(
TimePoint t0, Position pos0, Velocity vel0, Acceleration acc0, TimePoint t1)
{
const auto time_delta = t1.value() - t0.value();
return pos0.value() + vel0.value() * time_delta + 0.5f * acc0.value() * time_delta * time_delta;
}
float determine_future_position(
float t0, float pos0, float vel0, float acc0, float t1)
{
const auto time_delta = t1 - t0;
return pos0 + vel0 * time_delta + 0.5f * acc0 * time_delta * time_delta;
}Putting this example through compiler explorer, we can take a look at the generated assembly for each function. When compiling with -O1 level optimizations in clang, our generated assembly for the float implementation is
.LCPI1_0:
.long 0x3f000000
determine_future_position(float, float, float, float, float):
subss xmm4, xmm0
mulss xmm2, xmm4
addss xmm2, xmm1
mulss xmm3, dword ptr [rip + .LCPI1_0]
mulss xmm3, xmm4
mulss xmm3, xmm4
addss xmm3, xmm2
movaps xmm0, xmm3
retIn comparison, the assembly for the strong type implementation is
.LCPI0_0:
.long 0x3f000000
determine_future_position(strong_type<float, TimePointTag>, strong_type<float, PositionTag>, strong_type<float, VelocityTag>, strong_type<float, AccelerationTag>, strong_type<float, TimePointTag>):
subss xmm4, xmm0
mulss xmm2, xmm4
addss xmm2, xmm1
mulss xmm3, dword ptr [rip + .LCPI0_0]
mulss xmm3, xmm4
mulss xmm3, xmm4
addss xmm3, xmm2
movaps xmm0, xmm3
retIn other words, both functions have an identical implementation. This shows that using strong types in this case is indeed a zero overhead at runtime, at any reasonable level of optimization.
Next Steps
We now have a fully functional, zero overhead strong type system to generate arbitrary types from arbitrary underlying types. As we saw in the above implementation, we were required to extract the underlying type for some of these operations. In a future post, we’ll go over how we can use the Curiously Recurring Template Pattern (CRTP) to add traits to the strong types, permitting us to add functionality without resorting to pulling out the weak type. Until then, the full strong type implementation is available on my GitHub at https://github.com/Infirmarian/strong-types for exploration, including the type trait implementation.
If you got this far, thank you for reading my first post! This is my first post on Hot Path, so if you have comments, feedback, questions or suggestions for future topics, please drop them in the chat below. And if you found this useful, consider subscribing for future posts!

