Strong typing prevents implicit conversions between unrelated types, making it harder to accidentally mix or misuse variables in C++ code.
- ✅ Automatic propagation of underlying type operators and other skills
- ✅ No memory overhead
- ✅ No visible runtime overhead. [see more]
- ✅ Tested at compile time (strong guarantee against undefined behaviors)
- Usage
- Comparison with NamedType library by @joboccara
- Benchmarks
- Get it
- Build from source
- Supported Compilers
#include <stronger.hpp>
using Amount = stronger::strong_type<double, stronger::tag()>;
using Name = stronger::strong_type<std::string, stronger::tag()>;
Once declared, you can use Amount
and Name
in your code:
void print_info(const Name& name, Amount amount) {
std::println("{} gave {.2f}$", name, amount);
}
Name alice{"Alice"s};
Amount alice_donation{100.0};
print_info(alice, alice_donation);
// print_info("Paul"s, 250.0); // Error: cannot convert from std::string to Name, and from double to Amount
Arithmetics of the underlying type are automatically available:
Amount item1 = 100.0;
Amount item2 = 200.0;
Amount total = item1 + item2; // No implicit conversions here
You can use operator*
to access the underlying type.
double underlying_value = *alice_donation;
const std::string& underlying_string = *alice;
std::string underlying_string = *std::move(alice); // On r-value qualified strong types, the underlying type is moved.
You can use operator->
to access the underlying type methods and members.
size_t size = alice->size();
alice->append(" in wonderland");
You can use stronger::make_strong<S>(...)
to create a strong type
in-place.
using List = stronger::strong_type<std::vector<double>, stronger::tag()>;
const auto list = stronger::make_strong<List>(1.0, 2.0, 3.0, 4.0, 5.0);
Calling functions with bool arguments is usually confusing.
set_options(true, false); // What does this mean ?
Using stronger::strong_bool
in the function signature makes it easier to understand:
using enable_auto_save = stronger::strong_bool<stronger::tag()>;
using enable_always_show = stronger::strong_bool<stronger::tag()>;
void set_options(enable_auto_save autoSave, enable_always_show alwaysShow);
// Much easier to understand
set_options(enable_auto_save(true), enable_always_show(false));
// set_options(true, false); // Does not compile
// set_options(enable_auto_save(true), enable_always_show(false)); // Does not compile: swapped arguments
Note
strong_bool<Tag>
is a simple alias for strong_type<bool, Tag>
Important
operator&&
and operator||
are intentionally not
overloaded for strong_type
because they would not support short-circuit evaluation.
You cannot use &&
or ||
on strong_bools
directly.
Using stronger::weak_bool
allows implicit conversions to bool
, so that you can use logical
operators as if they were supported natively.
using enable_auto_save = stronger::weak_bool<stronger::tag()>;
using enable_always_show = stronger::weak_bool<stronger::tag()>;
bool result = autoSave && alwaysShow;
Drill-down behavior
of operator->
is implemented for raw pointers.
using NamePtr = stronger::strong_type<std::string*, stronger::tag()>;
std::string alice = "Alice"s;
const NamePtr namePtr(&alice);
alice->starts_with("A"); // Ok
You can use option drill_down
to enable this behavior on other types.
using NamePtr = stronger::strong_type<std::unique_ptr<std::string>, stronger::tag(), stronger::options::drill_down>;
const NamePtr alice(std::make_unique<std::string>("Alice"s));;
alice->starts_with("A"); // Ok
// alice->reset(); // Error, std::unique_ptr::reset is not available anymore
A strong type of iterable object is iterable as well.
using NameList = stronger::strong_type<std::vector<std::string>, stronger::tag()>;
const auto names = stronger::make_strong<NameList>("Alice"s, "Bob"s, "Charlie"s);
// begin() and end() are available
for (const std::string& name : names)
std::println("{} is present on the list.", name);
// operator[] is available
std::println("{} is the first on the list !", names[0ULL]);
It is also possible to work with strong functions. If you ever need it, here is the way:
template<typename Callable>
using Function = stronger::strong_type<Callable, stronger::tag()>;
template<typename Callable>
void print(Function<Callable> f, double x)
{
std::println("f({}) = {}", x, f(x));
}
auto f = stronger::make_strong<Function>([](double x) { return x * x; });
print(f, 10.0); // prints "f(10) = 100"
Some options can be enabled to customize the behavior of strong types.
using Price = stronger::strong_type<double, stronger::tag(), stronger::options::allow_implicit_conversion_to_underlying_type>;
using Name = stronger::strong_type<std::string, stronger::tag(), stronger::options::allow_implicit_construction>;
using NamePtr = stronger::strong_type<std::unique_ptr<std::string>, stronger::tag(), stronger::options::drill_down>;
Options can be combined.
The goal of tagging is to avoid this:
using Width = stronger::strong_type<double>;
using Height = stronger::strong_type<double>;
static_assert(std::is_same_v<Width, Height>); // We don't want this !
The first try could be to use a non-type template parameter in strong_type
that defaults to []{}
.
But this would cause link issues: the type would be different for the same
using
declaration in two different translation units. So we have to use a
template parameter that is not defaulted.
stronger::tag()
generates a unique size_t
, from hashing the result of
std::source_location::current()
.
Note that it does not hash the full path of the file, but only the file name, line number, column and function name.
stronger::tag()
should work fine in most cases. If you face link issues, you can
use stronger::tag(const char*)
.
The result of hashing is guaranteed to be the same no matter the source location.
using MyInt = stronger::strong_type<int, stronger::tag("MyInt")>;
Maybe you should prefer this option if you want to create a library that is released with binaries.
Comparison with NamedType library by @joboccara
stronger-cpp simplifies strong type declaration. It addresses several issues from NamedType, like ambiguous operators or suboptimal size of strong types.
It also compiles faster.
Operation | NamedType | stronger-cpp |
---|---|---|
Minimum C++ standard required | C++14 | C++23 |
sizeof (StrongType) |
>= sizeof(Underlying) [1] |
== sizeof(Underlying) |
Operators | Member functions | Friend functions |
Operator arguments | Always const & |
const & or value, depending on what is faster |
Inherit underlying type skills | Explicit | ✅ Automatic |
Making strong type unique | struct ActualTypeTag |
stronger::tag() |
Compilation time | Slower | ✅ About 20% faster |
Tests | Runtime tests | ✅ Fully compile-time tested [2] [3] |
Access underlying data | s.get() , *s , s->someFunction() |
*s , s->someFunction() |
Access underlying data from expired strong type | Makes a copy | ✅ Moves |
In-place construction | ❌ | stronger::make_strong<S>(...) |
Named arguments | ✅ | ❌ |
[1]: This will be == sizeof(Underlying)
only if empty base optimization is enabled.
[2]: This offers a strong guarantee against undefined behaviors.
[3]: Except formatting with std::format
because this feature is not available at compile time. It is
tested at runtime
anyway.
Assume a
and b
are both of type StrongType
.
Operation | NamedType | stronger-cpp |
---|---|---|
+a [1] |
✅ | ✅ |
-a [1] |
✅ | ✅ |
a + b [1] |
✅ | ✅ |
a - b [1] |
✅ | ✅ |
a * b [1] |
✅ | ✅ |
a / b [1] |
✅ | ✅ |
a % b [1] |
✅ | ✅ |
~a [1] |
✅ | ✅ |
a & b [1] |
✅ | ✅ |
a | b [1] |
✅ | ✅ |
a ^ b [1] |
✅ | ✅ |
a << b [1] |
✅ | ✅ |
a >> b [1] |
✅ | ✅ |
a += b [2] |
✅ | ✅ |
a -= b [2] |
✅ | ✅ |
a *= b [2] |
✅ | ✅ |
a /= b [2] |
✅ | ✅ |
a %= b [2] |
✅ | ✅ |
a &= b [2] |
✅ | ✅ |
a |= b [2] |
✅ | ✅ |
a ^= b [2] |
✅ | ✅ |
a <<= b [2] |
✅ | ✅ |
a >>= b [2] |
✅ | ✅ |
a == b |
✅ | ✅ |
a != b |
✅ | ✅ |
a < b |
✅ | ✅ |
a > b |
✅ | ✅ |
a <= b |
✅ | ✅ |
a >= b |
✅ | ✅ |
a <=> b |
❌ | ✅ |
++a [2] |
✅ | ✅ |
--a [2] |
✅ | ✅ |
a++ [1] |
✅ | ✅ |
a-- [1] |
✅ | ✅ |
!a |
❌ | ✅ |
a && b |
❌ | ❌ [3] |
a || b |
❌ | ❌ [3] |
StrongType(...) (static) |
❌ | ✅ |
a(...) |
❌ | ✅ |
StrongType[...] (static) |
❌ | ✅ |
a[...] |
❌ | ✅ |
os << a |
✅ | ✅ |
std::format |
❌ | ✅ |
std::hash |
✅ | ✅ |
[1]: Returns a StrongType
.
[2]: Returns a StrongType&
.
[3]: This is intentional because the overload would not support
short-circuit evaluation.
Use operators on the underlying type instead, or use weak_bool
.
Benchmarks are available in the tests/benchmarks folder.
xychart-beta
title "Compilation time (header only), 10 iterations"
x-axis [No strong type, NamedType, stronger-cpp]
y-axis "Compile time (s/10 iterations)" 0 --> 8
bar [2.2918355464935303, 7.023294687271118, 5.779611587524414]
According to the above chart, the compilation time of the stronger-cpp header is 22% faster than NamedType header.
Compiling this .cpp file
xychart-beta
title "Compilation time (complete file), 10 iterations"
x-axis [No strong type, NamedType, stronger-cpp]
y-axis "Compile time (s/10 iterations)" 0 --> 10
bar [2.2994439601898193, 8.531243324279785, 7.019076585769653]
According to the above chart, the compilation time of code using stronger-cpp is 22% faster than code using NamedType.
Compared to an implementation without strong types, the compilation is 3.1 times slower.
Performing operation on doubles (see here).
xychart-beta
title "Runtime performance (arithmetics on Strong<double>)"
x-axis [No strong type, NamedType, stronger-cpp]
y-axis "Total runtime (ms)" 0 --> 40
bar [37.0121, 37.1925, 37.0787]
An important point here is that the standard deviation for these behchmarks is close to 1 ms.
We can conclude that there is no visible runtime overhead when using a strong typing library, no matter if it is NamedType or stronger-cpp.
# Get any version (here getting the latest commit on master)
CPMAddPackage("gh:teskann/stronger-cpp#master")
target_link_libraries(YOUR_TARGET PRIVATE stronger-cpp::stronger-cpp)
You can download the latest release from here.
Clone the repository and make sure you have conan installed:
pip install conan
If this is your first time with Conan, you'll have to run
conan profile detect
conan install . --output-folder=build-debug -s build_type=Debug --build=missing
cmake --preset conan-debug
cmake --build build-debug
# Have fun
./build-debug/stronger_cpp_tests # Run tests
./build-debug/stronger_cpp_benchmarks # Run benchmarks
python ./tests/benchmarks/compile-time.py # Run compilation time benchmarks
conan install . --output-folder=build-release -s build_type=Release --build=missing
cmake --preset conan-release
cmake --build build-release
# Have fun
./build-release/stronger_cpp_tests # Run tests
./build-release/stronger_cpp_benchmarks # Run benchmarks
python ./tests/benchmarks/compile-time.py # Run compilation time benchmarks
Version | Supported ? |
---|---|
15 | ✅ |
14 | ✅ |
<14 | ❌ |
Version | Supported ? |
---|---|
19.44 | ✅ |
< 19.44 | Not tested |
Version | Supported ? |
---|---|
21 | Not tested |
20 | ❌ |
19 | ❌ |