C++: non-negative signed integral?
Published:
In this article, I’ll summarize my first experience playing with type traits in C++.
Note that as my first experience, I’ve decided not to dig too deep into cv-qualifications, being able to modify references of my wrapper, and all the sugar you’d get with a real wrapper.
What I have done is write a skeleton of an observer that shows me enough about templates and type traits for it to be a meaningful exercise – and I hope this simple example helps someone else grasp this concept too!
Awkward interfaces
Have you ever come across an interface that does a lazy job at constraining its inputs? Tell me, why does this API - with the comment, of course - exist? Would it be so hard to make bar
an unsigned integer? Actually, there are valid - but sparse - reasons to do so, but let me be dramatic.
/* argument must be non-zero */
void foo(int64_t bar);
After many failed attempts at rationalizing this, I just swallowed the bitter pill and decided to materialize the non-negativity comment! With that, we come to today’s goal: greedy non-negativity invariance enforcement for signed integral types.
Welcome type traits
Type traits is a fantastic rabbit-hole templated interface that lets you express ideas/constraints around types, and even modify the properties of types, all in compile time! These templates are defined in the <type_traits>
header.
A particularly useful metafunction in that header is std::enable_if
. Think of it as a compile-time switch for templates. This switch lets us tap into an idiomatic C++ rule - “Substitution Failure Is Not An Error” (SFINAE). Explaining SFINAE would take 5 of these blog posts, so the wiki redirection will have to do for now. std::enable_if
lets us define a typedef
to a given type if its template argument evaluates to a logically true value. In other words, if we specify constraints as part of std::enable_if
’s template arguments, we will cause substitution failures (hence omitting a template instantiation) if said constraints are not satisfied. Now, about constraints…
For this situation, I want to represent two constraints:
- The number must be of integral type.
- Integral is a big net, but notable residents are
bool
,char
,charX_t
,short
,int
,long
, and all of their signed and cv-qualified varieties… that’s a mouthful. - Really, I just need to avoid floating point types here.
std::is_integral
is my new best friend.
- Integral is a big net, but notable residents are
- The number must be of signed type.
- Why? Well, this also motivates why the interface accepted a signed integral type! Because unsigned integers give you zero information, maximum chaos. Imagine accidentally producing a negative number only to feed that into an unsigned container? No warnings, just overflow and vibes…
std::is_signed
is my other best friend.
Just like that, we have our building blocks to conditionally (std::enable_if
) build a wrapper around signed (std::is_signed
), integral (std::is_integral
) types. In C++, the template argument that represents these constraints is:
template<
class T,
typename = std::enable_if<
std::is_integral<T>::value &&
std::is_signed<T>::value>::type
>
Here’s what happened above - we have a template type T
. std::is_integral<T>::value
is true
if T
is an integral type - and likewise for std::is_signed
. Looking at the signature std::enable_if<bool B, class T = void>
, what happens is that if expression B
evaluates to true
, the templated class enable_if
will have a public typedef type
equal to T
. If expression B
evaluates to false
, there is no such typedef. Finally, typename = ...
just indicates the presence of an optional template parameter with no name and a default value. As you can guess, if that default value is nothing - there is a compiler error, which is exactly what happens if we have either an unsigned type, or a non-integral type, or both!
C++14 introduced some type aliases that make for succint expression of constraints like this, namely std::enable_if_t
, std::is_integral_v
, and std::is_signed_v
, where the a_x
just represents a::x
. Rewriting with these aliases, we get:
template<
class T,
typename = std::enable_if_t<
std::is_integral_v<T> &&
std::is_signed_v<T>>
>
What’s in a wrapper?
Finally, the wrapper around our signed, integral type is very short. Its only job is to greedily - at assignment/construction time - check that its not wrapping a negative number. This can be achieved as follows:
class non_negative {
public:
// What does a default construction mean here?
non_negative() = delete;
non_negative(T _number) {
// Negative number check!
if (_number < static_cast<T>(0)) {
throw std::runtime_error(
"Please enter non-negative number.");
}
number = _number;
}
private:
T number;
}
The last piece of this puzzle is to make this wrapper transparent to the user or the interfaces consuming it. Remember the simple duck test? If it walks like a duck and it talks like a duck… Right! As long as this wrapper can be implicitly converted to the type it represents, we have the duck. This is what the implicit (templated) conversion operator looks like:
inline operator T() const { return static_cast<T>(number); }
Ready for takeoff
Combining all the snippets above, this is the wrapper I have:
template<
class T,
typename = std::enable_if_t<
std::is_integral_v<T> &&
std::is_signed_v<T>>
>
class non_negative {
public:
// What does a default construction mean here?
non_negative() = delete;
non_negative(T _number) {
if (_number < static_cast<T>(0)) {
// Negative number check!
throw std::runtime_error(
"Please enter non-negative number.");
}
number = _number;
}
// Implicit conversion to underlying integral type
inline operator T() const { return static_cast<T>(number); }
private:
T number;
};
In about 22 lines of code, I learned about type traits, SFINAE, and got to think about better interface designs – particularly when the interface has semantically convoluted constraints! Not a bad use of 22 lines.
Here’s the link to my wrapper on a Github gist if you’d like to investigate further. Check out my Github profile (@aprotyas) for more of my coding – no, it’s not playing around with type traits all day!