Search code examples
c++unsigned-integer

Is there a way to make the compiler raise an error instead of allowing unsigned int wrapping?


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.


Solution

  • 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:

    • Implicit conversion like in your case will make the function callable with -1.
    • You now work with 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.

    An age should not be 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.

    Possible alternatives

    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 insist ...

    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.