Disclaimer: I am a C++ and programming noob
I was making a Profile
class as an exercise (from the book I am reading) which would hold the names and ages of profiles/people. The invariant I defined for the class state was: no duplicate usernames, no empty names (empty string) and no negative ages.
I thought preventing callers from adding negative ages to profiles could be done by representing the age as a unsigned int
like this:
typedef std::pair<std::string, unsigned int> name_age_pair;
void Profiles::add_profile(const std::string& username, const unsigned int age)
{
if (is_name_dup(username)) throw std::invalid_argument("Name is duplicate");
if (usernamename.empty()) throw std::invalid_argument("Name cannot be empty string");
all_profiles.push_back(name_age_pair(username, age));
}
I expected that the compiler would stop callers from passing negative integers as ages, but I was wrong. The integers just wraps around instead:
int main()
{
Profiles school_profiles{};
school_profiles.add_profile("Gunter", -1);
school_profiles.print_profiles(); // Gunter - 4294967295
}
Sure I could just change the type of age to a normal int and check if the age arguments is negative with a if-statement. But out of curiosity is there a way to make the compiler raise a error instead of allowing the wrapping when a negative argument is given.
You've run into the quintessential example of a precondition/contract: rejecting negative integers.
Using unsigned
integers is generally the wrong approach to deal with this problem, for multiple reasons:
-1
.unsigned
(modular) arithmetic within the function, and this is nonsensical because an age is not modular.Don't confuse unsigned
for a type that's just there to prevent negative values.
Similarly, std::sqrt
takes a double
, but this double
is not allowed to be negative.
We don't need and want a hypothetical unsigned double
to deal with this.
unsigned
To illustrate the last point, consider what happens when you subtract two ages: you get a time difference. This difference can be negative depending on which age is lower.
A negative age is also meaningful because an age is simply the time difference from the point of birth; a negative age is the time until someone is born. You can easily run into such cases when doing math with ages, and it would be very tedious to convert between signed/unsigned constantly.
CppCoreGuidelines has a rule ES.106: Don’t try to avoid negative values by using unsigned, with the following sample solution:
struct Positive {
int val;
Positive(int x) :val{x} { Assert(0 < x); }
operator int() { return val; }
};
int f(Positive arg) { return arg; }
int r1 = f(2);
int r2 = f(-2); // throws
This is obviously over-simplified, but if you really wanted to, you could encapsulate non-negativity in some wrapper class.
In most cases, using an assert
or an if
statement that can throw
to check if inputs are negative is totally fine, and any such wrapper is over-engineering.
If you're willing to wait a few years, there will also be C++26 contracts (presumably). P2900R6 is one of many proposals working on this. Among other features, this will allow you to specify pre-conditions and post-conditions for functions, such as:
void add_profile(const std::string& username, const int age) {
pre (age >= 0);
// ...
}
This is the ultimate solution to the problem, but it will take a few years until we can use it.
If you really insisted on using unsigned
integers despite everything said, the most simple C++20 solution would be:
void add_profile(const std::string& username, const std::unsigned_integral auto age)
This is an abbreviated function template where the age
parameter is any type that satisfies the std::unsigned_integral
concept.
If you provided say, int
, there would be no implicit conversion; instead, the constraints of this function template wouldn't be satisfied, resulting in an error.