Search code examples
c++implicit-conversionmodulusmodulo

Why does the C++ modulo operator return 0 for -1 % str.size()?


I'm confused why the following code produces this output:

#include <iostream>
#include <string>

using namespace std;

int main()
{
    int i = -1;
    string s = "abc";
    int j = s.size();
    int x = 1 % 3;
    int y = i % j; 
    int z = i % s.size(); 
    cout << s.size() << endl; // 3
    cout << x << endl;        // 1
    cout << y << endl;        // -1
    cout << z << endl;        // 0
}

Why is z = 0? Does it have to do with casting?


Solution

  • So, stripping down your code to a minimal example, you're asking why this prints 0:

    #include <iostream>
    #include <string>
    
    int main()
    {
        int a = -1;
        std::string::size_type b = 3; 
        int c = a % b;
        std::cout << c << '\n';
    }
    

    The primary operation in question here is this:

    a % b
    

    Per the standard,

    5.6 Multiplicative operators [expr.mul]

    1. The operands of * and / shall have arithmetic or unscoped enumeration type; the operands of % shall have integral or unscoped enumeration type. The usual arithmetic conversions are performed on the operands and determine the type of the result.

    So.. what about those "usual arithmetic conversions"? This is to mate the types of the two operands to a common type prior to performing the actual operation. The following are considered in order :

    • If both operands are integers, integer promotion is first performed on both operands. If after integer promotion the operands still have different types, conversion continues as follows:
      • If one operand has an unsigned type T whose conversion rank is at least as high as that of the other operand’s type, then the other operand is converted to type T.
      • Otherwise, one operand has a signed type T whose conversion rank is higher than that of the other operand’s type. The other operand is converted to type T only if type T is capable of representing all values of its previous type.
      • Otherwise, both operands are converted to the unsigned type that corresponds to the signed type T.

    That's a lot of legalize for what effectively says this:

    • You have two operands, a signed int and a std::string::size_type
    • The rank of std::string::size_type is greater than that of signed int
    • Therefore, the signed int operand is converted to type std::string:size_type prior to the operation being requested.

    So all that is left is the conversion, to wit, there is one more piece of legalize:

    4.7 Integral conversions [conv.integral]

    1. If the destination type is unsigned, the resulting value is the least unsigned integer congruent to the source integer (modulo 2n where n is the number of bits used to represent the unsigned type). [Note: In a two’s complement representation, this conversion is conceptual and there is no change in the bit pattern (if there is no truncation). —end note]

    That means, on a 32-bit std::string::size_type platform, you're going to get 232-1 as the converted value from int (-1).

    Which means...

    4294967295 % 3
    

    Which is... zero. If std::string::size_type is 64-bits, then everything above stays the same, save for the final calculation, which would be:

    18446744073709551615 % 3